@global-torque/invest-core 0.2.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.
- package/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/NOTICE.md +11 -0
- package/README.md +76 -0
- package/SECURITY.md +9 -0
- package/SUPPORT.md +6 -0
- package/dist/node/app/config.js +173 -0
- package/dist/node/helpers/text.js +37 -0
- package/dist/node/markdown/tableWrap.js +23 -0
- package/package.json +113 -0
- package/src/accreditation/status.ts +100 -0
- package/src/analytics/__tests__/analyticsBody.test.ts +50 -0
- package/src/analytics/analyticsBody.ts +270 -0
- package/src/app/config.test.ts +79 -0
- package/src/app/config.ts +329 -0
- package/src/decimal/__tests__/canonicalDecimal.test.ts +58 -0
- package/src/decimal/canonicalDecimal.ts +154 -0
- package/src/evm/__tests__/walletInfo.test.ts +488 -0
- package/src/evm/walletInfo.ts +625 -0
- package/src/filer/__tests__/documentFormatter.test.ts +208 -0
- package/src/filer/__tests__/publicImage.test.ts +42 -0
- package/src/filer/documentFormatter.ts +195 -0
- package/src/filer/publicImage.ts +120 -0
- package/src/form-validation/__tests__/general.test.ts +78 -0
- package/src/form-validation/__tests__/investment.test.ts +100 -0
- package/src/form-validation/ajv.ts +109 -0
- package/src/form-validation/constants.ts +22 -0
- package/src/form-validation/general.ts +114 -0
- package/src/form-validation/index.ts +5 -0
- package/src/form-validation/investment.ts +65 -0
- package/src/form-validation/rules.ts +35 -0
- package/src/formatting/__tests__/buildInfo.test.ts +19 -0
- package/src/formatting/__tests__/dateTime.test.ts +24 -0
- package/src/formatting/__tests__/display.test.ts +30 -0
- package/src/formatting/buildInfo.ts +31 -0
- package/src/formatting/dateTime.ts +43 -0
- package/src/formatting/display.ts +24 -0
- package/src/helpers/arrays.ts +11 -0
- package/src/helpers/currency.ts +19 -0
- package/src/helpers/formatters/formatToDate.ts +47 -0
- package/src/helpers/formatters/formatToNumber.ts +39 -0
- package/src/helpers/formatters/formatToPhone.ts +13 -0
- package/src/helpers/general.ts +164 -0
- package/src/helpers/model.ts +87 -0
- package/src/helpers/numberFormatter.ts +4 -0
- package/src/helpers/text.ts +51 -0
- package/src/index.ts +22 -0
- package/src/investment/__tests__/status.test.ts +59 -0
- package/src/investment/rawAmount.test.ts +23 -0
- package/src/investment/rawAmount.ts +81 -0
- package/src/investment/status.ts +56 -0
- package/src/kyc/__tests__/kycAlert.formatter.test.ts +47 -0
- package/src/kyc/__tests__/kycAlert.test.ts +47 -0
- package/src/kyc/__tests__/thirdPartyScreen.test.ts +16 -0
- package/src/kyc/kycAlert.ts +47 -0
- package/src/kyc/status.ts +109 -0
- package/src/kyc/thirdPartyScreen.ts +28 -0
- package/src/markdown/tableWrap.ts +29 -0
- package/src/notifications/shareFields.ts +15 -0
- package/src/offer/__tests__/metrics.test.ts +67 -0
- package/src/offer/formatter.ts +559 -0
- package/src/offer/metrics.ts +83 -0
- package/src/onboarding/__tests__/intents.test.ts +83 -0
- package/src/onboarding/intents.ts +128 -0
- package/src/profiles/__tests__/formatting.test.ts +24 -0
- package/src/profiles/avatarInitial.ts +5 -0
- package/src/profiles/formatting.ts +38 -0
- package/src/repository/__tests__/formatterCache.test.ts +45 -0
- package/src/repository/formatterCache.ts +56 -0
- package/src/wallet/__tests__/auth.test.ts +102 -0
- package/src/wallet/__tests__/operationPresentation.test.ts +94 -0
- package/src/wallet/__tests__/setupError.test.ts +32 -0
- package/src/wallet/auth.ts +491 -0
- package/src/wallet/operationPresentation.ts +106 -0
- package/src/wallet/setupError.ts +50 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatToFullDate,
|
|
3
|
+
} from '../formatting/dateTime.ts';
|
|
4
|
+
import {
|
|
5
|
+
capitalizeFirstLetter,
|
|
6
|
+
currency,
|
|
7
|
+
} from '../formatting/display.ts';
|
|
8
|
+
import {
|
|
9
|
+
assertCanonicalDecimalString,
|
|
10
|
+
assertPositiveCanonicalDecimalString,
|
|
11
|
+
compareCanonicalDecimals,
|
|
12
|
+
formatCanonicalUsd,
|
|
13
|
+
formatExactUsd,
|
|
14
|
+
} from '../decimal/canonicalDecimal.ts';
|
|
15
|
+
import {
|
|
16
|
+
calculateOfferFundedPercent,
|
|
17
|
+
calculateOfferMinimumInvestment,
|
|
18
|
+
isOfferClosingSoon,
|
|
19
|
+
isOfferFullyFunded,
|
|
20
|
+
isOfferFundingCompleted,
|
|
21
|
+
isOfferNewWithinDays,
|
|
22
|
+
isOfferSharesReached,
|
|
23
|
+
isRegD506cOffer,
|
|
24
|
+
} from './metrics.ts';
|
|
25
|
+
import {
|
|
26
|
+
IOfferFormatted, IOffer, OfferStatuses, PaymentScheduleTypes, VotingRightsTypes,
|
|
27
|
+
DividendType, DividendFrequencyTypes,
|
|
28
|
+
} from '@global-torque/domain-types/offerTypes';
|
|
29
|
+
|
|
30
|
+
type OfferApiImage = {
|
|
31
|
+
meta_data?: Partial<Record<PublicOfferImageSize, string>>;
|
|
32
|
+
url?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type PublicOfferImageSize = 'big' | 'small' | 'medium';
|
|
36
|
+
export type OfferImageResolver = (
|
|
37
|
+
source: number | string | null | undefined,
|
|
38
|
+
size: PublicOfferImageSize,
|
|
39
|
+
) => string | undefined;
|
|
40
|
+
|
|
41
|
+
export interface OfferFormatterOptions {
|
|
42
|
+
fallbackImage?: string;
|
|
43
|
+
resolveImage?: OfferImageResolver;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type OfferWithApiImage = IOffer & {
|
|
47
|
+
image?: OfferApiImage;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export class OfferFormatter {
|
|
51
|
+
private offer: IOffer;
|
|
52
|
+
private fallbackImage: string;
|
|
53
|
+
private resolveImage: OfferImageResolver;
|
|
54
|
+
|
|
55
|
+
constructor(offer?: IOffer, options: OfferFormatterOptions = {}) {
|
|
56
|
+
this.offer = offer ? { ...this.createDefaultOffer(), ...offer } : this.createDefaultOffer();
|
|
57
|
+
this.fallbackImage = options.fallbackImage ?? '';
|
|
58
|
+
this.resolveImage = options.resolveImage ?? (() => undefined);
|
|
59
|
+
assertCanonicalDecimalString(this.offer.price_per_share, 'price_per_share');
|
|
60
|
+
assertCanonicalDecimalString(this.offer.min_investment, 'min_investment');
|
|
61
|
+
assertCanonicalDecimalString(this.offer.total_shares, 'total_shares');
|
|
62
|
+
assertCanonicalDecimalString(this.offer.subscribed_shares, 'subscribed_shares');
|
|
63
|
+
assertCanonicalDecimalString(this.offer.confirmed_shares, 'confirmed_shares');
|
|
64
|
+
if (this.offer.id > 0) {
|
|
65
|
+
assertPositiveCanonicalDecimalString(this.offer.min_investment, 'min_investment');
|
|
66
|
+
}
|
|
67
|
+
if (this.offer.status === OfferStatuses.published) {
|
|
68
|
+
this.assertPublishedShareInvariants();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private assertPublishedShareInvariants(): void {
|
|
73
|
+
assertPositiveCanonicalDecimalString(this.offer.price_per_share, 'price_per_share');
|
|
74
|
+
assertPositiveCanonicalDecimalString(this.offer.min_investment, 'min_investment');
|
|
75
|
+
if (this.offer.fund_structure === 'open_ended') {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (compareCanonicalDecimals(this.offer.confirmed_shares, this.offer.subscribed_shares) > 0) {
|
|
79
|
+
throw new RangeError('confirmed_shares must not exceed subscribed_shares');
|
|
80
|
+
}
|
|
81
|
+
assertPositiveCanonicalDecimalString(this.offer.total_shares, 'total_shares');
|
|
82
|
+
if (this.offer.total_shares !== '0') {
|
|
83
|
+
if (compareCanonicalDecimals(this.offer.subscribed_shares, this.offer.total_shares) > 0) {
|
|
84
|
+
throw new RangeError('subscribed_shares must not exceed total_shares');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get amountRaisedFormatted() {
|
|
90
|
+
return currency(this.offer.amount_raised);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get targetRaiseFormatted() {
|
|
94
|
+
return this.offer.target_raise ? currency(this.offer.target_raise) : '-';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get pricePerShareFormatted() {
|
|
98
|
+
return formatCanonicalUsd(this.offer.price_per_share);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
get securityTypeFormatted() {
|
|
102
|
+
return this.offer.security_type ? capitalizeFirstLetter(this.offer.security_type) : '-';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get securityTypeTooltip(): string | undefined {
|
|
106
|
+
const securityTypeTooltips: Record<string, string> = {
|
|
107
|
+
'debt': 'A loan to the company that must be repaid with interest. Investors receive fixed interest payments on a set schedule until maturity.',
|
|
108
|
+
'convertible-note': 'A loan that may convert into equity (shares) in the future, usually during a financing round and often at a discount or with other benefits.',
|
|
109
|
+
'equity': 'Ownership shares in the company. Common stock may include voting rights, limited voting rights, or none, depending on the terms. Holders share in profits (if dividends are declared) and potential exit upside, but are last in priority if the company liquidates.',
|
|
110
|
+
'preferred-equity': 'Shares with priority over common stock for dividends and liquidation proceeds. Preferred stock may or may not include voting rights, depending on the terms.',
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return this.offer.security_type ? securityTypeTooltips[this.offer.security_type.toLowerCase()] : undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
get isSecurityTypeConvertibleNote(): boolean {
|
|
117
|
+
return this.offer.security_type === 'convertible-note';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
get isSecurityTypeConvertibleDebt(): boolean {
|
|
121
|
+
return this.offer.security_type === 'convertible_debt';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
get isSecurityTypeDebt(): boolean {
|
|
125
|
+
return this.offer.security_type === 'debt';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
get isSecurityTypeEquity(): boolean {
|
|
129
|
+
return this.offer.security_type === 'equity';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
get isSecurityTypePreferredEquity(): boolean {
|
|
133
|
+
return this.offer.security_type === 'preferred-equity';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
get isRegD506cOffer(): boolean {
|
|
137
|
+
return isRegD506cOffer(this.offer.reg_type);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private getApiImage(metaSize: PublicOfferImageSize): string | undefined {
|
|
141
|
+
const image = (this.offer as OfferWithApiImage)?.image;
|
|
142
|
+
const source = image?.meta_data?.[metaSize] || image?.url;
|
|
143
|
+
|
|
144
|
+
return source ? this.resolveImage(source, metaSize) : undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
get valuationLabel(): string {
|
|
148
|
+
if (this.isSecurityTypeDebt || this.isSecurityTypeConvertibleDebt || this.isSecurityTypeConvertibleNote) {
|
|
149
|
+
return 'Funding Goal:';
|
|
150
|
+
}
|
|
151
|
+
return 'Target Raise:';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
get valuationFormatted() {
|
|
155
|
+
return this.offer.valuation ? currency(this.offer.valuation) : '-';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
get preMoneyValuationFormatted(): string {
|
|
159
|
+
const value = this.offer.security_info?.pre_money_valuation;
|
|
160
|
+
if (value === undefined || value === null) {
|
|
161
|
+
return '-';
|
|
162
|
+
}
|
|
163
|
+
return currency(value);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
get approvedAtFormatted(): string {
|
|
167
|
+
return this.offer.approved_at
|
|
168
|
+
? formatToFullDate(this.offer.approved_at)
|
|
169
|
+
: '-';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
get closeAtFormatted(): string {
|
|
173
|
+
return this.offer.close_at
|
|
174
|
+
? formatToFullDate(this.offer.close_at)
|
|
175
|
+
: 'not closed';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
get statusFormatted() {
|
|
179
|
+
const statusMap = {
|
|
180
|
+
[OfferStatuses.new]: { text: 'New', color: 'blue' },
|
|
181
|
+
[OfferStatuses.draft]: { text: 'Draft', color: 'gray' },
|
|
182
|
+
[OfferStatuses.legal_review]: { text: 'Legal Review', color: 'yellow' },
|
|
183
|
+
[OfferStatuses.legal_accepted]: { text: 'Legal Accepted', color: 'green' },
|
|
184
|
+
[OfferStatuses.legal_declined]: { text: 'Legal Declined', color: 'red' },
|
|
185
|
+
[OfferStatuses.published]: { text: 'Published', color: 'green' },
|
|
186
|
+
[OfferStatuses.legal_closed]: { text: 'Legal Closed', color: 'orange' },
|
|
187
|
+
[OfferStatuses.closed_successfully]: { text: 'Closed Successfully', color: 'green' },
|
|
188
|
+
[OfferStatuses.closed_unsuccessfully]: { text: 'Closed Unsuccessfully', color: 'red' },
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
text: statusMap[this.offer.status as OfferStatuses]?.text || 'Unknown',
|
|
193
|
+
color: statusMap[this.offer.status as OfferStatuses]?.color || 'gray',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
getOfferImage(metaSize: PublicOfferImageSize = 'small'): string {
|
|
198
|
+
const apiImage = this.getApiImage(metaSize);
|
|
199
|
+
if (apiImage) {
|
|
200
|
+
return apiImage;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const imageID = this.offer?.image_link_id;
|
|
204
|
+
if (imageID && (imageID > 0)) {
|
|
205
|
+
return this.resolveImage(imageID, metaSize) ?? this.fallbackImage;
|
|
206
|
+
}
|
|
207
|
+
return this.fallbackImage;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
get offerFundedPercent() {
|
|
211
|
+
if (this.isOpenEnded) return 0;
|
|
212
|
+
return calculateOfferFundedPercent(this.offer.subscribed_shares, this.offer.total_shares);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
get isFullyFunded(): boolean {
|
|
216
|
+
if (this.isOpenEnded) return false;
|
|
217
|
+
return isOfferFullyFunded(this.offerFundedPercent);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
get isClosingSoon(): boolean {
|
|
221
|
+
if (this.isOpenEnded) return false;
|
|
222
|
+
return isOfferClosingSoon(this.offerFundedPercent);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
get isSharesReached(): boolean {
|
|
226
|
+
if (this.isOpenEnded) return false;
|
|
227
|
+
return isOfferSharesReached(
|
|
228
|
+
this.offer.total_shares,
|
|
229
|
+
this.offer.subscribed_shares,
|
|
230
|
+
this.offer.min_investment,
|
|
231
|
+
this.offer.price_per_share,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
get isOpenEnded(): boolean {
|
|
236
|
+
return this.offer.fund_structure === 'open_ended';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
get isDefaultImage(): boolean {
|
|
240
|
+
return !(this.offer?.image_link_id) && !this.getApiImage('small');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
get minInvestment(): string {
|
|
244
|
+
return calculateOfferMinimumInvestment(this.offer.min_investment);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
get minInvestmentFormatted(): string {
|
|
248
|
+
return formatExactUsd(this.minInvestment, 0);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
get isNew(): boolean {
|
|
252
|
+
return isOfferNewWithinDays(this.offer.approved_at);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
get tagText(): string {
|
|
256
|
+
if (this.isFullyFunded) return 'Funded';
|
|
257
|
+
if (this.isClosingSoon) return '🔥 Closing Soon';
|
|
258
|
+
return 'New';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
get tagBackground(): string {
|
|
262
|
+
if (this.isFullyFunded) return 'is--background-secondary-light';
|
|
263
|
+
if (this.isClosingSoon) return 'is--background-yellow-light';
|
|
264
|
+
return 'is--background-secondary-light';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
get showTag(): boolean {
|
|
268
|
+
return this.isFullyFunded || this.isClosingSoon || this.isNew;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
get isStatusNew(): boolean {
|
|
272
|
+
return this.offer.status === OfferStatuses.new;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
get isStatusDraft(): boolean {
|
|
276
|
+
return this.offer.status === OfferStatuses.draft;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
get isStatusLegalReview(): boolean {
|
|
280
|
+
return this.offer.status === OfferStatuses.legal_review;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
get isStatusLegalAccepted(): boolean {
|
|
284
|
+
return this.offer.status === OfferStatuses.legal_accepted;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
get isStatusLegalDeclined(): boolean {
|
|
288
|
+
return this.offer.status === OfferStatuses.legal_declined;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
get isStatusPublished(): boolean {
|
|
292
|
+
return this.offer.status === OfferStatuses.published;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
get isStatusLegalClosed(): boolean {
|
|
296
|
+
return this.offer.status === OfferStatuses.legal_closed;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
get isStatusClosedSuccessfully(): boolean {
|
|
300
|
+
return this.offer.status === OfferStatuses.closed_successfully;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
get isStatusClosedUnsuccessfully(): boolean {
|
|
304
|
+
return this.offer.status === OfferStatuses.closed_unsuccessfully;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
get isFundingCompleted(): boolean {
|
|
308
|
+
return isOfferFundingCompleted(this.offer.status);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Security Info Formatters
|
|
312
|
+
get votingRightsFormatted(): string | undefined {
|
|
313
|
+
const votingRights = this.offer.security_info?.voting_rights;
|
|
314
|
+
if (!votingRights) return undefined;
|
|
315
|
+
|
|
316
|
+
return VotingRightsTypes[votingRights as keyof typeof VotingRightsTypes] || votingRights;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
get liquidationPreferenceFormatted(): string | undefined {
|
|
320
|
+
if (this.isSecurityTypeEquity) return undefined;
|
|
321
|
+
return this.offer.security_info?.liquidation_preference;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
get dividendTypeFormatted(): string | undefined {
|
|
325
|
+
const dividendType = this.offer.security_info?.dividend_type;
|
|
326
|
+
if (!dividendType || this.isSecurityTypeEquity) return undefined;
|
|
327
|
+
|
|
328
|
+
return DividendType[dividendType as keyof typeof DividendType] || dividendType;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
get dividendRateFormatted(): string | undefined {
|
|
332
|
+
const dividendRate = this.offer.security_info?.dividend_rate;
|
|
333
|
+
if (!dividendRate) return undefined;
|
|
334
|
+
|
|
335
|
+
// Convert to string to handle both string and number types
|
|
336
|
+
const dividendRateStr = String(dividendRate);
|
|
337
|
+
if (dividendRateStr.toLowerCase() === 'none' || dividendRateStr.toLowerCase() === '') return undefined;
|
|
338
|
+
|
|
339
|
+
const numericValue = parseFloat(dividendRateStr);
|
|
340
|
+
if (Number.isNaN(numericValue)) return dividendRateStr;
|
|
341
|
+
|
|
342
|
+
return `${numericValue}%`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
get dividendPaymentFrequencyFormatted(): string | undefined {
|
|
346
|
+
const frequency = this.offer.security_info?.dividend_payment_frequency;
|
|
347
|
+
if (!frequency) return undefined;
|
|
348
|
+
|
|
349
|
+
return DividendFrequencyTypes[frequency as keyof typeof DividendFrequencyTypes] || frequency;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
get valuationCapFormatted(): string | undefined {
|
|
353
|
+
const valuationCap = this.offer.security_info?.cn_valuation_cap;
|
|
354
|
+
if (!valuationCap) return undefined;
|
|
355
|
+
|
|
356
|
+
// Parse the value and format as currency
|
|
357
|
+
const numericValue = parseFloat(valuationCap);
|
|
358
|
+
if (isNaN(numericValue)) return valuationCap; // Return as-is if not a number
|
|
359
|
+
|
|
360
|
+
return currency(numericValue);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
get discountRateFormatted(): string | undefined {
|
|
364
|
+
const discountRate = this.offer.security_info?.cn_discount_rate;
|
|
365
|
+
if (!discountRate) return undefined;
|
|
366
|
+
|
|
367
|
+
// Parse the value and format as percentage
|
|
368
|
+
const numericValue = parseFloat(discountRate);
|
|
369
|
+
if (isNaN(numericValue)) return discountRate; // Return as-is if not a number
|
|
370
|
+
|
|
371
|
+
return `${numericValue}%`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
get interestRateFormatted(): string | undefined {
|
|
375
|
+
const cnInterestRate = this.offer.security_info?.cn_interest_rate;
|
|
376
|
+
const debtInterestRate = this.offer.security_info?.debt_interest_rate;
|
|
377
|
+
|
|
378
|
+
const rate = cnInterestRate || debtInterestRate;
|
|
379
|
+
if (!rate) return undefined;
|
|
380
|
+
|
|
381
|
+
// Parse the value and format as percentage
|
|
382
|
+
const numericValue = parseFloat(rate);
|
|
383
|
+
if (isNaN(numericValue)) return rate; // Return as-is if not a number
|
|
384
|
+
|
|
385
|
+
return `${numericValue}%`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
get maturityDateFormatted(): string | undefined {
|
|
389
|
+
const cnMaturityDate = this.offer.security_info?.cn_maturity_date;
|
|
390
|
+
const debtMaturityDate = this.offer.security_info?.debt_maturity_date;
|
|
391
|
+
|
|
392
|
+
if (cnMaturityDate) {
|
|
393
|
+
return formatToFullDate(cnMaturityDate);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (debtMaturityDate) {
|
|
397
|
+
return formatToFullDate(debtMaturityDate);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
get interestRateApyFormatted(): string | undefined {
|
|
404
|
+
const interestRateApy = this.offer.security_info?.interest_rate_apy;
|
|
405
|
+
if (!interestRateApy) return undefined;
|
|
406
|
+
|
|
407
|
+
// Parse the value and format as percentage
|
|
408
|
+
const numericValue = parseFloat(interestRateApy);
|
|
409
|
+
if (isNaN(numericValue)) return interestRateApy; // Return as-is if not a number
|
|
410
|
+
|
|
411
|
+
return `${numericValue}%`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
get paymentScheduleFormatted(): string | undefined {
|
|
415
|
+
const paymentSchedule = this.offer.security_info?.debt_payment_schedule;
|
|
416
|
+
if (!paymentSchedule) return undefined;
|
|
417
|
+
|
|
418
|
+
return PaymentScheduleTypes[paymentSchedule as keyof typeof PaymentScheduleTypes] || paymentSchedule;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
get termLengthFormatted(): string | undefined {
|
|
422
|
+
const termLength = this.offer.security_info?.debt_term_length;
|
|
423
|
+
const termUnit = this.offer.security_info?.debt_term_unit;
|
|
424
|
+
|
|
425
|
+
if (!termLength) return undefined;
|
|
426
|
+
|
|
427
|
+
return `${termLength} ${termUnit}`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private createDefaultOffer(): IOffer {
|
|
431
|
+
return {
|
|
432
|
+
id: 0,
|
|
433
|
+
amount_raised: 0,
|
|
434
|
+
approved_at: '',
|
|
435
|
+
close_at: '',
|
|
436
|
+
confirmed_shares: '0',
|
|
437
|
+
image_link_id: 0,
|
|
438
|
+
min_investment: '0',
|
|
439
|
+
name: '',
|
|
440
|
+
legal_name: '',
|
|
441
|
+
price_per_share: '0',
|
|
442
|
+
seo_description: '',
|
|
443
|
+
seo_title: '',
|
|
444
|
+
slug: '',
|
|
445
|
+
description: '',
|
|
446
|
+
highlights: '',
|
|
447
|
+
status: 'draft',
|
|
448
|
+
subscribed_shares: '0',
|
|
449
|
+
title: '',
|
|
450
|
+
total_shares: '0',
|
|
451
|
+
fund_structure: 'closed_ended',
|
|
452
|
+
valuation: 0,
|
|
453
|
+
target_raise: 0,
|
|
454
|
+
additional_details: '',
|
|
455
|
+
website: '',
|
|
456
|
+
security_type: '',
|
|
457
|
+
city: '',
|
|
458
|
+
state: '',
|
|
459
|
+
data: {
|
|
460
|
+
wire_to: '',
|
|
461
|
+
swift_id: '',
|
|
462
|
+
custodian: '',
|
|
463
|
+
account_number: '',
|
|
464
|
+
routing_number: '',
|
|
465
|
+
apy: '',
|
|
466
|
+
distribution_frequency: '',
|
|
467
|
+
investment_strategy: '',
|
|
468
|
+
estimated_hold_period: '',
|
|
469
|
+
},
|
|
470
|
+
security_info: {
|
|
471
|
+
voting_rights: '',
|
|
472
|
+
liquidation_preference: '',
|
|
473
|
+
dividend_type: '',
|
|
474
|
+
dividend_rate: '',
|
|
475
|
+
dividend_payment_frequency: '',
|
|
476
|
+
cn_valuation_cap: '',
|
|
477
|
+
cn_discount_rate: '',
|
|
478
|
+
cn_interest_rate: '',
|
|
479
|
+
cn_maturity_date: '',
|
|
480
|
+
interest_rate_apy: '',
|
|
481
|
+
debt_payment_schedule: '',
|
|
482
|
+
debt_maturity_date: '',
|
|
483
|
+
debt_interest_rate: '',
|
|
484
|
+
debt_term_length: '',
|
|
485
|
+
debt_term_unit: '',
|
|
486
|
+
pre_money_valuation: 0,
|
|
487
|
+
},
|
|
488
|
+
linkedin: '',
|
|
489
|
+
facebook: '',
|
|
490
|
+
twitter: '',
|
|
491
|
+
github: '',
|
|
492
|
+
instagram: '',
|
|
493
|
+
telegram: '',
|
|
494
|
+
mastodon: '',
|
|
495
|
+
reg_type: '',
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
format(): IOfferFormatted {
|
|
500
|
+
return {
|
|
501
|
+
...this.offer,
|
|
502
|
+
amountRaisedFormatted: this.amountRaisedFormatted,
|
|
503
|
+
targetRaiseFormatted: this.targetRaiseFormatted,
|
|
504
|
+
pricePerShareFormatted: this.pricePerShareFormatted,
|
|
505
|
+
valuationFormatted: this.valuationFormatted,
|
|
506
|
+
preMoneyValuationFormatted: this.preMoneyValuationFormatted,
|
|
507
|
+
securityTypeFormatted: this.securityTypeFormatted,
|
|
508
|
+
securityTypeTooltip: this.securityTypeTooltip,
|
|
509
|
+
statusFormatted: this.statusFormatted,
|
|
510
|
+
approvedAtFormatted: this.approvedAtFormatted,
|
|
511
|
+
closeAtFormatted: this.closeAtFormatted,
|
|
512
|
+
isDefaultImage: this.isDefaultImage,
|
|
513
|
+
minInvestment: this.minInvestment,
|
|
514
|
+
minInvestmentFormatted: this.minInvestmentFormatted,
|
|
515
|
+
offerFundedPercent: this.offerFundedPercent,
|
|
516
|
+
isOpenEnded: this.isOpenEnded,
|
|
517
|
+
isFullyFunded: this.isFullyFunded,
|
|
518
|
+
isClosingSoon: this.isClosingSoon,
|
|
519
|
+
isSharesReached: this.isSharesReached,
|
|
520
|
+
imageBig: this.getOfferImage('big'),
|
|
521
|
+
imageSmall: this.getOfferImage('small'),
|
|
522
|
+
imageMedium: this.getOfferImage('medium'),
|
|
523
|
+
tagText: this.tagText,
|
|
524
|
+
tagBackground: this.tagBackground,
|
|
525
|
+
showTag: this.showTag,
|
|
526
|
+
isNew: this.isNew,
|
|
527
|
+
isStatusNew: this.isStatusNew,
|
|
528
|
+
isStatusDraft: this.isStatusDraft,
|
|
529
|
+
isStatusLegalReview: this.isStatusLegalReview,
|
|
530
|
+
isStatusLegalAccepted: this.isStatusLegalAccepted,
|
|
531
|
+
isStatusLegalDeclined: this.isStatusLegalDeclined,
|
|
532
|
+
isStatusPublished: this.isStatusPublished,
|
|
533
|
+
isStatusLegalClosed: this.isStatusLegalClosed,
|
|
534
|
+
isStatusClosedSuccessfully: this.isStatusClosedSuccessfully,
|
|
535
|
+
isStatusClosedUnsuccessfully: this.isStatusClosedUnsuccessfully,
|
|
536
|
+
isFundingCompleted: this.isFundingCompleted,
|
|
537
|
+
// Security Info Formatted Fields
|
|
538
|
+
votingRightsFormatted: this.votingRightsFormatted,
|
|
539
|
+
liquidationPreferenceFormatted: this.liquidationPreferenceFormatted,
|
|
540
|
+
dividendTypeFormatted: this.dividendTypeFormatted,
|
|
541
|
+
dividendRateFormatted: this.dividendRateFormatted,
|
|
542
|
+
dividendPaymentFrequencyFormatted: this.dividendPaymentFrequencyFormatted,
|
|
543
|
+
valuationCapFormatted: this.valuationCapFormatted,
|
|
544
|
+
discountRateFormatted: this.discountRateFormatted,
|
|
545
|
+
interestRateFormatted: this.interestRateFormatted,
|
|
546
|
+
maturityDateFormatted: this.maturityDateFormatted,
|
|
547
|
+
interestRateApyFormatted: this.interestRateApyFormatted,
|
|
548
|
+
paymentScheduleFormatted: this.paymentScheduleFormatted,
|
|
549
|
+
termLengthFormatted: this.termLengthFormatted,
|
|
550
|
+
isSecurityTypeConvertibleDebt: this.isSecurityTypeConvertibleDebt,
|
|
551
|
+
isSecurityTypeConvertibleNote: this.isSecurityTypeConvertibleNote,
|
|
552
|
+
isSecurityTypeDebt: this.isSecurityTypeDebt,
|
|
553
|
+
isSecurityTypeEquity: this.isSecurityTypeEquity,
|
|
554
|
+
isSecurityTypePreferredEquity: this.isSecurityTypePreferredEquity,
|
|
555
|
+
valuationLabel: this.valuationLabel,
|
|
556
|
+
isRegD506cOffer: this.isRegD506cOffer,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { OfferStatuses } from '@global-torque/domain-types/offerTypes';
|
|
2
|
+
import {
|
|
3
|
+
CANONICAL_DECIMAL_SCALE,
|
|
4
|
+
canonicalDecimalToScaled,
|
|
5
|
+
compareCanonicalDecimals,
|
|
6
|
+
} from '../decimal/canonicalDecimal.ts';
|
|
7
|
+
|
|
8
|
+
export function calculateOfferFundedPercent(
|
|
9
|
+
subscribedShares: string,
|
|
10
|
+
totalShares: string,
|
|
11
|
+
): number {
|
|
12
|
+
const total = canonicalDecimalToScaled(totalShares);
|
|
13
|
+
const subscribed = canonicalDecimalToScaled(subscribedShares);
|
|
14
|
+
if (total === 0n) {
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const numerator = subscribed * 100n;
|
|
19
|
+
const wholePercent = numerator / total;
|
|
20
|
+
const remainder = numerator % total;
|
|
21
|
+
|
|
22
|
+
return Number(
|
|
23
|
+
numerator > 85n * total || remainder === 0n
|
|
24
|
+
? wholePercent
|
|
25
|
+
: wholePercent + 1n,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const isOfferFullyFunded = (offerFundedPercent: number): boolean => (
|
|
30
|
+
offerFundedPercent >= 100
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
export const isOfferClosingSoon = (offerFundedPercent: number): boolean => (
|
|
34
|
+
offerFundedPercent > 90 && !isOfferFullyFunded(offerFundedPercent)
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export const isOfferSharesReached = (
|
|
38
|
+
totalShares: string,
|
|
39
|
+
subscribedShares: string,
|
|
40
|
+
minInvestmentAmount: string,
|
|
41
|
+
pricePerShare = '1',
|
|
42
|
+
): boolean => {
|
|
43
|
+
if (totalShares === '0') return false;
|
|
44
|
+
if (compareCanonicalDecimals(subscribedShares, totalShares) > 0) return true;
|
|
45
|
+
const remainingShares = canonicalDecimalToScaled(totalShares)
|
|
46
|
+
- canonicalDecimalToScaled(subscribedShares);
|
|
47
|
+
const remainingAmountScaled = remainingShares * canonicalDecimalToScaled(pricePerShare);
|
|
48
|
+
const minimumAmountScaled = canonicalDecimalToScaled(minInvestmentAmount)
|
|
49
|
+
* (10n ** BigInt(CANONICAL_DECIMAL_SCALE));
|
|
50
|
+
return remainingAmountScaled < minimumAmountScaled;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const calculateOfferMinimumInvestment = (
|
|
54
|
+
minInvestmentAmount: string,
|
|
55
|
+
_pricePerShare?: string,
|
|
56
|
+
): string => minInvestmentAmount;
|
|
57
|
+
|
|
58
|
+
export function isOfferNewWithinDays(
|
|
59
|
+
approvedAt?: string | Date | null,
|
|
60
|
+
dayWindow = 2,
|
|
61
|
+
now: Date = new Date(),
|
|
62
|
+
): boolean {
|
|
63
|
+
if (!approvedAt) return false;
|
|
64
|
+
|
|
65
|
+
const approvedDate = new Date(approvedAt);
|
|
66
|
+
const diffMs = now.getTime() - approvedDate.getTime();
|
|
67
|
+
const diffDays = diffMs / (1000 * 3600 * 24);
|
|
68
|
+
|
|
69
|
+
return diffDays < dayWindow;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function isRegD506cOffer(regType?: string | null): boolean {
|
|
73
|
+
if (!regType) return false;
|
|
74
|
+
|
|
75
|
+
const normalized = regType.toLowerCase();
|
|
76
|
+
return normalized.includes('506(c)') && normalized.includes('reg d');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const isOfferFundingCompleted = (status?: string | null): boolean => (
|
|
80
|
+
status === OfferStatuses.legal_closed
|
|
81
|
+
|| status === OfferStatuses.closed_successfully
|
|
82
|
+
|| status === OfferStatuses.closed_unsuccessfully
|
|
83
|
+
);
|