@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.
Files changed (75) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE.md +11 -0
  4. package/README.md +76 -0
  5. package/SECURITY.md +9 -0
  6. package/SUPPORT.md +6 -0
  7. package/dist/node/app/config.js +173 -0
  8. package/dist/node/helpers/text.js +37 -0
  9. package/dist/node/markdown/tableWrap.js +23 -0
  10. package/package.json +113 -0
  11. package/src/accreditation/status.ts +100 -0
  12. package/src/analytics/__tests__/analyticsBody.test.ts +50 -0
  13. package/src/analytics/analyticsBody.ts +270 -0
  14. package/src/app/config.test.ts +79 -0
  15. package/src/app/config.ts +329 -0
  16. package/src/decimal/__tests__/canonicalDecimal.test.ts +58 -0
  17. package/src/decimal/canonicalDecimal.ts +154 -0
  18. package/src/evm/__tests__/walletInfo.test.ts +488 -0
  19. package/src/evm/walletInfo.ts +625 -0
  20. package/src/filer/__tests__/documentFormatter.test.ts +208 -0
  21. package/src/filer/__tests__/publicImage.test.ts +42 -0
  22. package/src/filer/documentFormatter.ts +195 -0
  23. package/src/filer/publicImage.ts +120 -0
  24. package/src/form-validation/__tests__/general.test.ts +78 -0
  25. package/src/form-validation/__tests__/investment.test.ts +100 -0
  26. package/src/form-validation/ajv.ts +109 -0
  27. package/src/form-validation/constants.ts +22 -0
  28. package/src/form-validation/general.ts +114 -0
  29. package/src/form-validation/index.ts +5 -0
  30. package/src/form-validation/investment.ts +65 -0
  31. package/src/form-validation/rules.ts +35 -0
  32. package/src/formatting/__tests__/buildInfo.test.ts +19 -0
  33. package/src/formatting/__tests__/dateTime.test.ts +24 -0
  34. package/src/formatting/__tests__/display.test.ts +30 -0
  35. package/src/formatting/buildInfo.ts +31 -0
  36. package/src/formatting/dateTime.ts +43 -0
  37. package/src/formatting/display.ts +24 -0
  38. package/src/helpers/arrays.ts +11 -0
  39. package/src/helpers/currency.ts +19 -0
  40. package/src/helpers/formatters/formatToDate.ts +47 -0
  41. package/src/helpers/formatters/formatToNumber.ts +39 -0
  42. package/src/helpers/formatters/formatToPhone.ts +13 -0
  43. package/src/helpers/general.ts +164 -0
  44. package/src/helpers/model.ts +87 -0
  45. package/src/helpers/numberFormatter.ts +4 -0
  46. package/src/helpers/text.ts +51 -0
  47. package/src/index.ts +22 -0
  48. package/src/investment/__tests__/status.test.ts +59 -0
  49. package/src/investment/rawAmount.test.ts +23 -0
  50. package/src/investment/rawAmount.ts +81 -0
  51. package/src/investment/status.ts +56 -0
  52. package/src/kyc/__tests__/kycAlert.formatter.test.ts +47 -0
  53. package/src/kyc/__tests__/kycAlert.test.ts +47 -0
  54. package/src/kyc/__tests__/thirdPartyScreen.test.ts +16 -0
  55. package/src/kyc/kycAlert.ts +47 -0
  56. package/src/kyc/status.ts +109 -0
  57. package/src/kyc/thirdPartyScreen.ts +28 -0
  58. package/src/markdown/tableWrap.ts +29 -0
  59. package/src/notifications/shareFields.ts +15 -0
  60. package/src/offer/__tests__/metrics.test.ts +67 -0
  61. package/src/offer/formatter.ts +559 -0
  62. package/src/offer/metrics.ts +83 -0
  63. package/src/onboarding/__tests__/intents.test.ts +83 -0
  64. package/src/onboarding/intents.ts +128 -0
  65. package/src/profiles/__tests__/formatting.test.ts +24 -0
  66. package/src/profiles/avatarInitial.ts +5 -0
  67. package/src/profiles/formatting.ts +38 -0
  68. package/src/repository/__tests__/formatterCache.test.ts +45 -0
  69. package/src/repository/formatterCache.ts +56 -0
  70. package/src/wallet/__tests__/auth.test.ts +102 -0
  71. package/src/wallet/__tests__/operationPresentation.test.ts +94 -0
  72. package/src/wallet/__tests__/setupError.test.ts +32 -0
  73. package/src/wallet/auth.ts +491 -0
  74. package/src/wallet/operationPresentation.ts +106 -0
  75. package/src/wallet/setupError.ts +50 -0
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { InvestKycTypes } from '@global-torque/domain-types/kycTypes';
3
+ import { formatKycAlertModel } from '@global-torque/invest-core/kyc/kycAlert';
4
+
5
+ describe('formatKycAlertModel', () => {
6
+ it.each([
7
+ [InvestKycTypes.none, true, 'error', 'Verify Identity', 'Verify Identity'],
8
+ [InvestKycTypes.new, true, 'error', 'Verify Identity', 'Verify Identity'],
9
+ [InvestKycTypes.pending, true, 'error', 'Finish Your KYC', 'Continue'],
10
+ [InvestKycTypes.in_progress, true, 'info', 'Verification In Progress', undefined],
11
+ [InvestKycTypes.declined, true, 'error', 'Verification Declined', undefined],
12
+ ])('maps %s into the normalized alert model', (status, show, variant, title, buttonText) => {
13
+ const model = formatKycAlertModel({ status });
14
+
15
+ expect(model.show).toBe(show);
16
+ expect(model.variant).toBe(variant);
17
+ expect(model.title).toBe(title);
18
+ expect(model.buttonText).toBe(buttonText);
19
+ });
20
+
21
+ it('hides the alert for approved KYC status', () => {
22
+ const model = formatKycAlertModel({ status: InvestKycTypes.approved });
23
+
24
+ expect(model.show).toBe(false);
25
+ expect(model.title).toBe('');
26
+ expect(model.buttonText).toBeUndefined();
27
+ });
28
+
29
+ it('hides the alert when the profile is already approved even if the status is stale', () => {
30
+ const model = formatKycAlertModel({
31
+ status: InvestKycTypes.pending,
32
+ isKycApproved: true,
33
+ });
34
+
35
+ expect(model.show).toBe(false);
36
+ });
37
+
38
+ it('mirrors Plaid loading into CTA loading and disabled state', () => {
39
+ const model = formatKycAlertModel({
40
+ status: InvestKycTypes.pending,
41
+ isPlaidLoading: true,
42
+ });
43
+
44
+ expect(model.isLoading).toBe(true);
45
+ expect(model.isDisabled).toBe(true);
46
+ });
47
+ });
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { InvestKycTypes } from '../status.ts';
3
+ import { formatKycAlertModel } from '../kycAlert.ts';
4
+
5
+ describe('formatKycAlertModel', () => {
6
+ it.each([
7
+ [InvestKycTypes.none, true, 'error', 'Verify Identity', 'Verify Identity'],
8
+ [InvestKycTypes.new, true, 'error', 'Verify Identity', 'Verify Identity'],
9
+ [InvestKycTypes.pending, true, 'error', 'Finish Your KYC', 'Continue'],
10
+ [InvestKycTypes.in_progress, true, 'info', 'Verification In Progress', undefined],
11
+ [InvestKycTypes.declined, true, 'error', 'Verification Declined', undefined],
12
+ ])('maps %s into the normalized alert model', (status, show, variant, title, buttonText) => {
13
+ const model = formatKycAlertModel({ status });
14
+
15
+ expect(model.show).toBe(show);
16
+ expect(model.variant).toBe(variant);
17
+ expect(model.title).toBe(title);
18
+ expect(model.buttonText).toBe(buttonText);
19
+ });
20
+
21
+ it('hides the alert for approved KYC status', () => {
22
+ const model = formatKycAlertModel({ status: InvestKycTypes.approved });
23
+
24
+ expect(model.show).toBe(false);
25
+ expect(model.title).toBe('');
26
+ expect(model.buttonText).toBeUndefined();
27
+ });
28
+
29
+ it('hides the alert when the profile is already approved even if the status is stale', () => {
30
+ const model = formatKycAlertModel({
31
+ status: InvestKycTypes.pending,
32
+ isKycApproved: true,
33
+ });
34
+
35
+ expect(model.show).toBe(false);
36
+ });
37
+
38
+ it('mirrors Plaid loading into CTA loading and disabled state', () => {
39
+ const model = formatKycAlertModel({
40
+ status: InvestKycTypes.pending,
41
+ isPlaidLoading: true,
42
+ });
43
+
44
+ expect(model.isLoading).toBe(true);
45
+ expect(model.isDisabled).toBe(true);
46
+ });
47
+ });
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { formatKycThirdPartyScreen } from '../thirdPartyScreen.ts';
3
+
4
+ describe('formatKycThirdPartyScreen', () => {
5
+ it('returns the launching screen copy', () => {
6
+ expect(formatKycThirdPartyScreen('launching')).toEqual({
7
+ title: 'Opening identity verification',
8
+ description: 'Please wait while we connect you to our verification partner.',
9
+ });
10
+ });
11
+
12
+ it('uses the same fallback copy for terminal error states', () => {
13
+ expect(formatKycThirdPartyScreen('invalidToken')).toEqual(formatKycThirdPartyScreen('error'));
14
+ expect(formatKycThirdPartyScreen('incomplete')).toEqual(formatKycThirdPartyScreen('error'));
15
+ });
16
+ });
@@ -0,0 +1,47 @@
1
+ import {
2
+ InvestKycTypes,
3
+ KycAlerts,
4
+ KycTextStatuses,
5
+ type KycAlertModel,
6
+ } from './status.ts';
7
+
8
+ export interface KycAlertFormatterInput {
9
+ isKycApproved?: boolean | null;
10
+ isPlaidLoading?: boolean;
11
+ status?: InvestKycTypes | null;
12
+ }
13
+
14
+ export const createHiddenKycAlertModel = (): KycAlertModel => ({
15
+ show: false,
16
+ variant: 'error',
17
+ title: '',
18
+ description: '',
19
+ buttonText: undefined,
20
+ isLoading: false,
21
+ isDisabled: false,
22
+ });
23
+
24
+ export const formatKycAlertModel = ({
25
+ isKycApproved = false,
26
+ isPlaidLoading = false,
27
+ status = InvestKycTypes.none,
28
+ }: KycAlertFormatterInput): KycAlertModel => {
29
+ const resolvedStatus = status ?? InvestKycTypes.none;
30
+
31
+ if (isKycApproved || resolvedStatus === InvestKycTypes.approved) {
32
+ return createHiddenKycAlertModel();
33
+ }
34
+
35
+ const alert = KycAlerts[resolvedStatus];
36
+ const statusText = KycTextStatuses[resolvedStatus];
37
+
38
+ return {
39
+ show: true,
40
+ variant: resolvedStatus === InvestKycTypes.in_progress ? 'info' : 'error',
41
+ title: alert.title,
42
+ description: alert.description,
43
+ buttonText: statusText.button ? statusText.text : undefined,
44
+ isLoading: isPlaidLoading,
45
+ isDisabled: isPlaidLoading,
46
+ };
47
+ };
@@ -0,0 +1,109 @@
1
+ import { InvestKycTypes as InvestKycTypesValue } from '@global-torque/domain-types/kycTypes';
2
+ import type { InvestKycTypes as InvestKycStatus } from '@global-torque/domain-types/kycTypes';
3
+
4
+ export { InvestKycTypes } from '@global-torque/domain-types/kycTypes';
5
+
6
+ export type KycAlertVariant = 'error' | 'info';
7
+
8
+ export type KycThirdPartyStatus =
9
+ | 'idle'
10
+ | 'launching'
11
+ | 'success'
12
+ | 'incomplete'
13
+ | 'invalidToken'
14
+ | 'error';
15
+
16
+ export interface KycAlertModel {
17
+ show: boolean;
18
+ variant: KycAlertVariant;
19
+ title: string;
20
+ description: string;
21
+ buttonText?: string;
22
+ isLoading: boolean;
23
+ isDisabled: boolean;
24
+ }
25
+
26
+ export interface KycThirdPartyScreenModel {
27
+ title: string;
28
+ description: string;
29
+ }
30
+
31
+ export interface KycTextStatus {
32
+ text: string;
33
+ class: string;
34
+ button?: boolean;
35
+ tooltip?: string;
36
+ mobileText?: string;
37
+ }
38
+
39
+ export interface KycAlertContent {
40
+ title: string;
41
+ class: string;
42
+ description: string;
43
+ button?: boolean;
44
+ tooltip?: string;
45
+ }
46
+
47
+ const VERIFY_IDENTITY_TEXT: KycTextStatus = {
48
+ text: 'Verify Identity',
49
+ mobileText: 'Verify',
50
+ class: 'none',
51
+ button: true,
52
+ };
53
+
54
+ const VERIFY_IDENTITY_ALERT: KycAlertContent = {
55
+ title: 'Verify Identity',
56
+ description: 'Complete identity verification to unlock investing access for this profile.',
57
+ class: 'none',
58
+ button: true,
59
+ };
60
+
61
+ export const KycTextStatuses: Record<InvestKycStatus, KycTextStatus> = {
62
+ [InvestKycTypesValue.none]: VERIFY_IDENTITY_TEXT,
63
+ [InvestKycTypesValue.new]: VERIFY_IDENTITY_TEXT,
64
+ [InvestKycTypesValue.declined]: {
65
+ text: 'Declined',
66
+ class: 'failed',
67
+ },
68
+ [InvestKycTypesValue.pending]: {
69
+ text: 'Continue',
70
+ class: 'pending',
71
+ button: true,
72
+ },
73
+ [InvestKycTypesValue.in_progress]: {
74
+ text: 'Verification In Progress',
75
+ mobileText: 'In Progress',
76
+ class: 'pending',
77
+ tooltip: 'Pending until all associated parties complete KYC.',
78
+ },
79
+ [InvestKycTypesValue.approved]: {
80
+ text: 'Verified',
81
+ class: 'success',
82
+ },
83
+ };
84
+
85
+ export const KycAlerts: Record<InvestKycStatus, KycAlertContent> = {
86
+ [InvestKycTypesValue.none]: VERIFY_IDENTITY_ALERT,
87
+ [InvestKycTypesValue.new]: VERIFY_IDENTITY_ALERT,
88
+ [InvestKycTypesValue.pending]: {
89
+ title: 'Finish Your KYC',
90
+ description: 'Complete the remaining identity verification steps so we can continue reviewing your submission.',
91
+ class: 'pending',
92
+ button: true,
93
+ },
94
+ [InvestKycTypesValue.in_progress]: {
95
+ title: 'Verification In Progress',
96
+ description: 'Your KYC review is in progress. We will notify you as soon as there is an update.',
97
+ class: 'pending',
98
+ },
99
+ [InvestKycTypesValue.declined]: {
100
+ title: 'Verification Declined',
101
+ description: 'Your identity verification was declined. <a href="#contact-us-dialog" class="is--link-1" data-action="contact-us">Contact support</a> and we will help resolve the issue.',
102
+ class: 'failed',
103
+ },
104
+ [InvestKycTypesValue.approved]: {
105
+ title: 'Identity Verified',
106
+ description: 'Your identity has been successfully verified for this investment profile.',
107
+ class: 'success',
108
+ },
109
+ };
@@ -0,0 +1,28 @@
1
+ import type {
2
+ KycThirdPartyScreenModel,
3
+ KycThirdPartyStatus,
4
+ } from './status.ts';
5
+
6
+ const FALLBACK_SCREEN: KycThirdPartyScreenModel = {
7
+ title: 'We could not complete identity verification',
8
+ description: 'Please retry from the original verification link. If the problem continues, contact support.',
9
+ };
10
+
11
+ const SCREEN_BY_STATUS: Record<KycThirdPartyStatus, KycThirdPartyScreenModel> = {
12
+ idle: FALLBACK_SCREEN,
13
+ launching: {
14
+ title: 'Opening identity verification',
15
+ description: 'Please wait while we connect you to our verification partner.',
16
+ },
17
+ success: {
18
+ title: 'Thank you for completing KYC',
19
+ description: 'Your KYC process is now complete.',
20
+ },
21
+ incomplete: FALLBACK_SCREEN,
22
+ invalidToken: FALLBACK_SCREEN,
23
+ error: FALLBACK_SCREEN,
24
+ };
25
+
26
+ export function formatKycThirdPartyScreen(status: KycThirdPartyStatus): KycThirdPartyScreenModel {
27
+ return SCREEN_BY_STATUS[status];
28
+ }
@@ -0,0 +1,29 @@
1
+ import type MarkdownIt from 'markdown-it';
2
+
3
+ /**
4
+ * Plugin to wrap tables in a div with overflow scrolling
5
+ * This allows tables to be horizontally scrollable on smaller screens
6
+ * Matches the VTable component's wrapper structure
7
+ */
8
+ export const tableWrap = (md: MarkdownIt): void => {
9
+ // Store the original table renderers if they exist
10
+ const defaultTableOpen = md.renderer.rules.table_open || ((tokens, idx, options, env, self) => {
11
+ return self.renderToken(tokens, idx, options);
12
+ });
13
+ const defaultTableClose = md.renderer.rules.table_close || ((tokens, idx, options, env, self) => {
14
+ return self.renderToken(tokens, idx, options);
15
+ });
16
+
17
+ // Override table_open to add opening wrapper div
18
+ md.renderer.rules.table_open = (tokens, idx, options, env, self) => {
19
+ return '<div class="v-table__wrap">\n' + defaultTableOpen(tokens, idx, options, env, self);
20
+ };
21
+
22
+ // Override table_close to add closing wrapper div
23
+ md.renderer.rules.table_close = (tokens, idx, options, env, self) => {
24
+ return defaultTableClose(tokens, idx, options, env, self) + '\n</div>';
25
+ };
26
+ };
27
+
28
+ export default tableWrap;
29
+
@@ -0,0 +1,15 @@
1
+ import type {
2
+ INotification,
3
+ INotificationDataFields,
4
+ } from '@global-torque/domain-types/notificationsTypes';
5
+ import { assertCanonicalDecimalString } from '../decimal/canonicalDecimal.ts';
6
+
7
+ export function assertNotificationShareFields(notification: INotification): void {
8
+ const fields = notification?.data?.fields ?? {} as INotificationDataFields;
9
+ if (fields.confirmed_shares !== undefined) {
10
+ assertCanonicalDecimalString(fields.confirmed_shares, 'confirmed_shares');
11
+ }
12
+ if (fields.subscribed_shares !== undefined) {
13
+ assertCanonicalDecimalString(fields.subscribed_shares, 'subscribed_shares');
14
+ }
15
+ }
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { OfferStatuses } from '@global-torque/domain-types/offerTypes';
3
+ import {
4
+ calculateOfferFundedPercent,
5
+ calculateOfferMinimumInvestment,
6
+ isOfferClosingSoon,
7
+ isOfferFullyFunded,
8
+ isOfferFundingCompleted,
9
+ isOfferNewWithinDays,
10
+ isOfferSharesReached,
11
+ isRegD506cOffer,
12
+ } from '../metrics.ts';
13
+
14
+ describe('offer metrics', () => {
15
+ it('uses existing funded-percent rounding behavior', () => {
16
+ expect(calculateOfferFundedPercent('0', '0')).toBe(0);
17
+ expect(calculateOfferFundedPercent('421', '1000')).toBe(43);
18
+ expect(calculateOfferFundedPercent('856', '1000')).toBe(85);
19
+ expect(calculateOfferFundedPercent('999', '1000')).toBe(99);
20
+ });
21
+
22
+ it('derives funding progress booleans', () => {
23
+ expect(isOfferFullyFunded(100)).toBe(true);
24
+ expect(isOfferFullyFunded(99)).toBe(false);
25
+ expect(isOfferClosingSoon(91)).toBe(true);
26
+ expect(isOfferClosingSoon(100)).toBe(false);
27
+ });
28
+
29
+ it('calculates minimum investment and remaining-share cutoff', () => {
30
+ expect(calculateOfferMinimumInvestment('25', '12')).toBe('25');
31
+ expect(calculateOfferMinimumInvestment('0', '12')).toBe('0');
32
+ expect(isOfferSharesReached('100', '96', '50', '12')).toBe(true);
33
+ expect(isOfferSharesReached('100', '95', '50', '12')).toBe(false);
34
+ expect(isOfferSharesReached('0', '99999999999999999999', '1')).toBe(false);
35
+ });
36
+
37
+ it('keeps fractional and unsafe share values exact', () => {
38
+ expect(calculateOfferFundedPercent(
39
+ '9007199254740992.000000000000000001',
40
+ '18014398509481984.000000000000000002',
41
+ )).toBe(50);
42
+ expect(calculateOfferMinimumInvestment('2000.000000000000000001', '12.5'))
43
+ .toBe('2000.000000000000000001');
44
+ expect(isOfferSharesReached(
45
+ '9007199254740993.000000000000000001',
46
+ '9007199254740992.999999999999999999',
47
+ '0.000000000000000026',
48
+ '12.5',
49
+ )).toBe(true);
50
+ });
51
+
52
+ it('detects new and Reg D 506(c) offers', () => {
53
+ const now = new Date('2026-04-10T12:00:00.000Z');
54
+
55
+ expect(isOfferNewWithinDays('2026-04-09T12:00:00.000Z', 2, now)).toBe(true);
56
+ expect(isOfferNewWithinDays('2026-04-07T11:59:00.000Z', 2, now)).toBe(false);
57
+ expect(isOfferNewWithinDays('', 2, now)).toBe(false);
58
+ expect(isRegD506cOffer('Reg D 506(c)')).toBe(true);
59
+ expect(isRegD506cOffer('Reg CF')).toBe(false);
60
+ });
61
+
62
+ it('detects completed funding statuses', () => {
63
+ expect(isOfferFundingCompleted(OfferStatuses.legal_closed)).toBe(true);
64
+ expect(isOfferFundingCompleted(OfferStatuses.closed_successfully)).toBe(true);
65
+ expect(isOfferFundingCompleted(OfferStatuses.published)).toBe(false);
66
+ });
67
+ });