@tuturuuu/utils 0.0.3 → 0.6.1
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 +313 -0
- package/biome.json +5 -0
- package/jsr.json +8 -8
- package/package.json +63 -32
- package/src/__tests__/ai-temp-auth.test.ts +309 -0
- package/src/__tests__/api-proxy-guard.test.ts +1451 -0
- package/src/__tests__/app-url.test.ts +270 -0
- package/src/__tests__/avatar-url.test.ts +97 -0
- package/src/__tests__/color-helper.test.ts +179 -0
- package/src/__tests__/constants.test.ts +351 -0
- package/src/__tests__/crypto.test.ts +107 -0
- package/src/__tests__/date-helper.test.ts +408 -0
- package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
- package/src/__tests__/format.test.ts +317 -0
- package/src/__tests__/html-sanitizer.test.ts +360 -0
- package/src/__tests__/interest-calculator.test.ts +336 -0
- package/src/__tests__/interest-detector.test.ts +222 -0
- package/src/__tests__/label-colors.test.ts +241 -0
- package/src/__tests__/name-helper.test.ts +158 -0
- package/src/__tests__/node-diff.test.ts +576 -0
- package/src/__tests__/notification-service.test.ts +210 -0
- package/src/__tests__/onboarding-helper.test.ts +331 -0
- package/src/__tests__/path-helper.test.ts +152 -0
- package/src/__tests__/permissions.test.tsx +81 -0
- package/src/__tests__/request-emoji-limit.test.ts +172 -0
- package/src/__tests__/search-helper.test.ts +51 -0
- package/src/__tests__/storage-display-name.test.ts +37 -0
- package/src/__tests__/storage-path.test.ts +238 -0
- package/src/__tests__/tag-utils.test.ts +205 -0
- package/src/__tests__/task-description-yjs-state.test.ts +581 -0
- package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
- package/src/__tests__/task-helper-create-task.test.ts +129 -0
- package/src/__tests__/task-helpers.test.ts +464 -0
- package/src/__tests__/task-overrides.test.ts +305 -0
- package/src/__tests__/task-reorder-cache.test.ts +74 -0
- package/src/__tests__/task-sort-keys.test.ts +36 -0
- package/src/__tests__/task-transformers.test.ts +62 -0
- package/src/__tests__/text-helper.test.ts +776 -0
- package/src/__tests__/time-helper.test.ts +70 -0
- package/src/__tests__/time-tracker-period.test.ts +55 -0
- package/src/__tests__/timezone.test.ts +117 -0
- package/src/__tests__/upstash-rest.test.ts +77 -0
- package/src/__tests__/uuid-helper.test.ts +133 -0
- package/src/__tests__/workspace-helper.test.ts +859 -0
- package/src/__tests__/workspace-limits.test.ts +255 -0
- package/src/__tests__/yjs-helper.test.ts +581 -0
- package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
- package/src/abuse-protection/__tests__/edge.test.ts +136 -0
- package/src/abuse-protection/__tests__/index.test.ts +562 -0
- package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
- package/src/abuse-protection/backend-rate-limit.ts +44 -0
- package/src/abuse-protection/constants.ts +117 -0
- package/src/abuse-protection/edge.ts +223 -0
- package/src/abuse-protection/index.ts +1545 -0
- package/src/abuse-protection/reputation.ts +587 -0
- package/src/abuse-protection/types.ts +97 -0
- package/src/abuse-protection/user-agent.ts +124 -0
- package/src/abuse-protection/user-suspension.ts +231 -0
- package/src/ai-temp-auth.ts +315 -0
- package/src/api-proxy-guard.ts +965 -0
- package/src/app-url.ts +96 -0
- package/src/avatar-url.ts +64 -0
- package/src/break-duration.ts +84 -0
- package/src/calendar-auth-token.test.ts +37 -0
- package/src/calendar-auth-token.ts +19 -0
- package/src/calendar-sync-coordination.md +197 -0
- package/src/calendar-utils.test.ts +169 -0
- package/src/calendar-utils.ts +91 -0
- package/src/color-helper.ts +110 -0
- package/src/common/nextjs.tsx +99 -0
- package/src/common/scan.tsx +15 -0
- package/src/configs/reports.ts +160 -0
- package/src/constants.ts +85 -0
- package/src/crypto.ts +21 -0
- package/src/currencies.ts +97 -0
- package/src/date-helper.ts +313 -0
- package/src/editor/convert-to-task.ts +264 -0
- package/src/editor/index.ts +5 -0
- package/src/email/__tests__/client.test.ts +141 -0
- package/src/email/__tests__/validation.test.ts +46 -0
- package/src/email/client.ts +92 -0
- package/src/email/server.ts +128 -0
- package/src/email/validation.ts +11 -0
- package/src/encryption/__tests__/calendar-events.test.ts +411 -0
- package/src/encryption/__tests__/configuration.test.ts +114 -0
- package/src/encryption/__tests__/field-encryption.test.ts +232 -0
- package/src/encryption/__tests__/key-generation.test.ts +30 -0
- package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
- package/src/encryption/__tests__/test-helpers.ts +22 -0
- package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
- package/src/encryption/encryption-service.ts +343 -0
- package/src/encryption/index.ts +25 -0
- package/src/encryption/types.ts +57 -0
- package/src/exchange-rates.ts +49 -0
- package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
- package/src/feature-flags/core.ts +322 -0
- package/src/feature-flags/data.ts +16 -0
- package/src/feature-flags/default.ts +18 -0
- package/src/feature-flags/index.ts +7 -0
- package/src/feature-flags/requestable-features.ts +79 -0
- package/src/feature-flags/types.ts +4 -0
- package/src/fetcher.ts +2 -0
- package/src/finance/index.ts +4 -0
- package/src/finance/interest-calculator.ts +456 -0
- package/src/finance/interest-detector.ts +141 -0
- package/src/finance/transform-invoice-results.ts +219 -0
- package/src/finance/wallet-permissions.test.ts +169 -0
- package/src/finance/wallet-permissions.ts +82 -0
- package/src/format.ts +120 -1
- package/src/generated/platform-build-metadata.ts +11 -0
- package/src/hooks/use-platform.ts +64 -0
- package/src/html-sanitizer.ts +155 -0
- package/src/internal-domains.ts +497 -0
- package/src/keyboard-preset.ts +109 -0
- package/src/label-colors.ts +213 -0
- package/src/launchable-apps.test.ts +126 -0
- package/src/launchable-apps.ts +490 -0
- package/src/name-helper.ts +269 -0
- package/src/next-config.test.ts +234 -0
- package/src/next-config.ts +203 -0
- package/src/node-diff.ts +375 -0
- package/src/notification-service.ts +379 -0
- package/src/nova/scores/__tests__/calculate.test.ts +254 -0
- package/src/nova/scores/calculate.ts +132 -0
- package/src/nova/submissions/check-permission.ts +132 -0
- package/src/onboarding-helper.ts +213 -0
- package/src/path-helper.ts +93 -0
- package/src/permissions.tsx +1170 -0
- package/src/plan-helpers.test.ts +188 -0
- package/src/plan-helpers.ts +80 -0
- package/src/platform-release.test.ts +74 -0
- package/src/platform-release.ts +155 -0
- package/src/portless.ts +124 -0
- package/src/priority-styles.ts +42 -0
- package/src/request-emoji-limit.ts +335 -0
- package/src/search-helper.ts +18 -0
- package/src/search.test.ts +89 -0
- package/src/search.ts +355 -0
- package/src/storage-display-name.ts +30 -0
- package/src/storage-path.ts +147 -0
- package/src/tag-utils.ts +159 -0
- package/src/task/reorder.ts +245 -0
- package/src/task/transformers.ts +149 -0
- package/src/task-date-timezone.ts +133 -0
- package/src/task-description-content.ts +240 -0
- package/src/task-helper/board.ts +193 -0
- package/src/task-helper/bulk-actions.ts +564 -0
- package/src/task-helper/personal-external-staging.ts +21 -0
- package/src/task-helper/recycle-bin.ts +202 -0
- package/src/task-helper/relationships.ts +346 -0
- package/src/task-helper/shared.ts +109 -0
- package/src/task-helper/sort-keys.ts +337 -0
- package/src/task-helper/task-hooks-basic.ts +342 -0
- package/src/task-helper/task-hooks-move.ts +264 -0
- package/src/task-helper/task-operations.ts +278 -0
- package/src/task-helper.ts +12 -0
- package/src/task-helpers.ts +241 -0
- package/src/task-list-status.ts +62 -0
- package/src/task-overrides.ts +82 -0
- package/src/task-snapshot.ts +374 -0
- package/src/text-diff.ts +81 -0
- package/src/text-helper.ts +537 -0
- package/src/time-helper.ts +63 -0
- package/src/time-tracker-period.ts +73 -0
- package/src/timeblock-helper.ts +418 -0
- package/src/timezone.ts +190 -0
- package/src/timezones.json +1271 -0
- package/src/upstash-rest.ts +56 -0
- package/src/user-helper.ts +296 -0
- package/src/uuid-helper.ts +11 -0
- package/src/workspace-handle.ts +10 -0
- package/src/workspace-helper.ts +1408 -0
- package/src/workspace-limits.ts +68 -0
- package/src/yjs-helper.ts +217 -0
- package/src/yjs-task-description.ts +81 -0
- package/tsconfig.json +3 -5
- package/tsconfig.typecheck.json +33 -0
- package/vitest.config.ts +36 -0
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -2
- package/dist/index.mjs.map +0 -1
- package/eslint.config.mjs +0 -20
- package/rollup.config.js +0 -41
- package/src/index.ts +0 -1
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import type { WalletInterestRate } from '@tuturuuu/types';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
calculateDailyInterest,
|
|
6
|
+
calculateInterest,
|
|
7
|
+
formatDateString,
|
|
8
|
+
getDaysUntilInterestStarts,
|
|
9
|
+
getInterestStartDate,
|
|
10
|
+
getMonthToDateRange,
|
|
11
|
+
getNextBusinessDay,
|
|
12
|
+
getRateForDate,
|
|
13
|
+
getYearToDateRange,
|
|
14
|
+
isBusinessDay,
|
|
15
|
+
isHoliday,
|
|
16
|
+
isWeekend,
|
|
17
|
+
parseDateString,
|
|
18
|
+
projectInterest,
|
|
19
|
+
} from '../finance/interest-calculator';
|
|
20
|
+
|
|
21
|
+
describe('interest-calculator', () => {
|
|
22
|
+
describe('isWeekend', () => {
|
|
23
|
+
it('should return true for Saturday', () => {
|
|
24
|
+
// 2025-01-25 is a Saturday
|
|
25
|
+
const saturday = new Date(2025, 0, 25);
|
|
26
|
+
expect(isWeekend(saturday)).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should return true for Sunday', () => {
|
|
30
|
+
// 2025-01-26 is a Sunday
|
|
31
|
+
const sunday = new Date(2025, 0, 26);
|
|
32
|
+
expect(isWeekend(sunday)).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should return false for weekdays', () => {
|
|
36
|
+
// 2025-01-27 is a Monday
|
|
37
|
+
const monday = new Date(2025, 0, 27);
|
|
38
|
+
expect(isWeekend(monday)).toBe(false);
|
|
39
|
+
|
|
40
|
+
// 2025-01-29 is a Wednesday
|
|
41
|
+
const wednesday = new Date(2025, 0, 29);
|
|
42
|
+
expect(isWeekend(wednesday)).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('isHoliday', () => {
|
|
47
|
+
const holidays = new Set(['2025-01-01', '2025-01-28']);
|
|
48
|
+
|
|
49
|
+
it('should return true for holidays', () => {
|
|
50
|
+
const newYear = new Date(2025, 0, 1);
|
|
51
|
+
expect(isHoliday(newYear, holidays)).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should return false for non-holidays', () => {
|
|
55
|
+
const normalDay = new Date(2025, 0, 15);
|
|
56
|
+
expect(isHoliday(normalDay, holidays)).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('isBusinessDay', () => {
|
|
61
|
+
const holidays = new Set(['2025-01-01']);
|
|
62
|
+
|
|
63
|
+
it('should return false for weekends', () => {
|
|
64
|
+
const saturday = new Date(2025, 0, 25);
|
|
65
|
+
expect(isBusinessDay(saturday, holidays)).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should return false for holidays', () => {
|
|
69
|
+
const holiday = new Date(2025, 0, 1);
|
|
70
|
+
expect(isBusinessDay(holiday, holidays)).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should return true for normal weekdays', () => {
|
|
74
|
+
const monday = new Date(2025, 0, 6);
|
|
75
|
+
expect(isBusinessDay(monday, holidays)).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('getNextBusinessDay', () => {
|
|
80
|
+
const holidays = new Set(['2025-01-01']);
|
|
81
|
+
|
|
82
|
+
it('should return next day if it is a business day', () => {
|
|
83
|
+
// Wednesday -> Thursday
|
|
84
|
+
const wednesday = new Date(2025, 0, 8);
|
|
85
|
+
const result = getNextBusinessDay(wednesday, holidays);
|
|
86
|
+
expect(formatDateString(result)).toBe('2025-01-09');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should skip weekends', () => {
|
|
90
|
+
// Friday -> Monday
|
|
91
|
+
const friday = new Date(2025, 0, 24);
|
|
92
|
+
const result = getNextBusinessDay(friday, holidays);
|
|
93
|
+
expect(formatDateString(result)).toBe('2025-01-27');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should skip holidays', () => {
|
|
97
|
+
// Dec 31, 2024 -> Jan 2, 2025 (skipping Jan 1 holiday)
|
|
98
|
+
const dec31 = new Date(2024, 11, 31);
|
|
99
|
+
const result = getNextBusinessDay(dec31, holidays);
|
|
100
|
+
expect(formatDateString(result)).toBe('2025-01-02');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('getInterestStartDate', () => {
|
|
105
|
+
const holidays = new Set<string>();
|
|
106
|
+
|
|
107
|
+
it('should start interest next business day for Monday deposit', () => {
|
|
108
|
+
const monday = new Date(2025, 0, 27);
|
|
109
|
+
const result = getInterestStartDate(monday, holidays);
|
|
110
|
+
expect(formatDateString(result)).toBe('2025-01-28');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should start interest on Monday for Friday deposit', () => {
|
|
114
|
+
const friday = new Date(2025, 0, 24);
|
|
115
|
+
const result = getInterestStartDate(friday, holidays);
|
|
116
|
+
expect(formatDateString(result)).toBe('2025-01-27');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should start interest on Tuesday for Saturday deposit', () => {
|
|
120
|
+
const saturday = new Date(2025, 0, 25);
|
|
121
|
+
const result = getInterestStartDate(saturday, holidays);
|
|
122
|
+
expect(formatDateString(result)).toBe('2025-01-27');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('getDaysUntilInterestStarts', () => {
|
|
127
|
+
const holidays = new Set<string>();
|
|
128
|
+
|
|
129
|
+
it('should return 0 if interest already started', () => {
|
|
130
|
+
const pastDeposit = new Date(2025, 0, 20);
|
|
131
|
+
const today = new Date(2025, 0, 27);
|
|
132
|
+
const result = getDaysUntilInterestStarts(pastDeposit, holidays, today);
|
|
133
|
+
expect(result).toBe(0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should return positive days for future start', () => {
|
|
137
|
+
// Deposited today (Monday), interest starts tomorrow (Tuesday)
|
|
138
|
+
const today = new Date(2025, 0, 27);
|
|
139
|
+
const result = getDaysUntilInterestStarts(today, holidays, today);
|
|
140
|
+
expect(result).toBe(1);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('getRateForDate', () => {
|
|
145
|
+
const rates: WalletInterestRate[] = [
|
|
146
|
+
{
|
|
147
|
+
id: '1',
|
|
148
|
+
config_id: 'cfg1',
|
|
149
|
+
annual_rate: 4.0,
|
|
150
|
+
effective_from: '2025-01-01',
|
|
151
|
+
effective_to: '2025-06-30',
|
|
152
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: '2',
|
|
156
|
+
config_id: 'cfg1',
|
|
157
|
+
annual_rate: 4.5,
|
|
158
|
+
effective_from: '2025-07-01',
|
|
159
|
+
effective_to: null,
|
|
160
|
+
created_at: '2025-07-01T00:00:00Z',
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
it('should return correct rate for date within range', () => {
|
|
165
|
+
const date = new Date(2025, 2, 15); // March 15
|
|
166
|
+
expect(getRateForDate(date, rates)).toBe(4.0);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should return correct rate for date in open-ended range', () => {
|
|
170
|
+
const date = new Date(2025, 8, 15); // September 15
|
|
171
|
+
expect(getRateForDate(date, rates)).toBe(4.5);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should return null for date before any rate', () => {
|
|
175
|
+
const date = new Date(2024, 11, 15); // December 2024
|
|
176
|
+
expect(getRateForDate(date, rates)).toBe(null);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('calculateDailyInterest', () => {
|
|
181
|
+
it('should calculate correct daily interest using Momo formula', () => {
|
|
182
|
+
// Balance: 10,000,000 VND, Rate: 4%
|
|
183
|
+
// Daily = floor(10,000,000 × (0.04 / 365)) = floor(1095.89) = 1095
|
|
184
|
+
const balance = 10_000_000;
|
|
185
|
+
const rate = 4.0;
|
|
186
|
+
expect(calculateDailyInterest(balance, rate)).toBe(1095);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should return 0 for zero balance', () => {
|
|
190
|
+
expect(calculateDailyInterest(0, 4.0)).toBe(0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should return 0 for negative balance', () => {
|
|
194
|
+
expect(calculateDailyInterest(-1000, 4.0)).toBe(0);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should floor the result', () => {
|
|
198
|
+
// Check that we're flooring, not rounding
|
|
199
|
+
const balance = 1_000_000;
|
|
200
|
+
const rate = 4.0;
|
|
201
|
+
// Daily = floor(1,000,000 × (0.04 / 365)) = floor(109.589) = 109
|
|
202
|
+
expect(calculateDailyInterest(balance, rate)).toBe(109);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('calculateInterest', () => {
|
|
207
|
+
const rates: WalletInterestRate[] = [
|
|
208
|
+
{
|
|
209
|
+
id: '1',
|
|
210
|
+
config_id: 'cfg1',
|
|
211
|
+
annual_rate: 4.0,
|
|
212
|
+
effective_from: '2025-01-01',
|
|
213
|
+
effective_to: null,
|
|
214
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
215
|
+
},
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
it('should calculate interest over a date range', () => {
|
|
219
|
+
const result = calculateInterest({
|
|
220
|
+
transactions: [],
|
|
221
|
+
rates,
|
|
222
|
+
holidays: [],
|
|
223
|
+
fromDate: '2025-01-06', // Monday
|
|
224
|
+
toDate: '2025-01-10', // Friday
|
|
225
|
+
initialBalance: 10_000_000,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// 5 business days
|
|
229
|
+
expect(result.businessDaysCount).toBe(5);
|
|
230
|
+
expect(result.totalInterest).toBeGreaterThan(0);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should handle weekend correctly', () => {
|
|
234
|
+
const result = calculateInterest({
|
|
235
|
+
transactions: [],
|
|
236
|
+
rates,
|
|
237
|
+
holidays: [],
|
|
238
|
+
fromDate: '2025-01-25', // Saturday
|
|
239
|
+
toDate: '2025-01-26', // Sunday
|
|
240
|
+
initialBalance: 10_000_000,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
expect(result.businessDaysCount).toBe(0);
|
|
244
|
+
expect(result.totalInterest).toBe(0);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should compound interest', () => {
|
|
248
|
+
const result = calculateInterest({
|
|
249
|
+
transactions: [],
|
|
250
|
+
rates,
|
|
251
|
+
holidays: [],
|
|
252
|
+
fromDate: '2025-01-06',
|
|
253
|
+
toDate: '2025-01-10',
|
|
254
|
+
initialBalance: 10_000_000,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Interest should compound - final balance > initial + (daily × days)
|
|
258
|
+
const nonCompoundedInterest = calculateDailyInterest(10_000_000, 4.0) * 5;
|
|
259
|
+
expect(result.totalInterest).toBeGreaterThanOrEqual(
|
|
260
|
+
nonCompoundedInterest
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe('projectInterest', () => {
|
|
266
|
+
it('should project future interest', () => {
|
|
267
|
+
const projections = projectInterest({
|
|
268
|
+
currentBalance: 10_000_000,
|
|
269
|
+
currentRate: 4.0,
|
|
270
|
+
holidays: [],
|
|
271
|
+
days: 7,
|
|
272
|
+
startDate: '2025-01-06', // Monday
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect(projections).toHaveLength(7);
|
|
276
|
+
expect(projections[0]?.projectedBalance).toBeGreaterThanOrEqual(
|
|
277
|
+
10_000_000
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should skip weekends in projections', () => {
|
|
282
|
+
const projections = projectInterest({
|
|
283
|
+
currentBalance: 10_000_000,
|
|
284
|
+
currentRate: 4.0,
|
|
285
|
+
holidays: [],
|
|
286
|
+
days: 7,
|
|
287
|
+
startDate: '2025-01-25', // Saturday
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// First two days (Sat, Sun) should have 0 interest
|
|
291
|
+
expect(projections[0]?.projectedDailyInterest).toBe(0);
|
|
292
|
+
expect(projections[1]?.projectedDailyInterest).toBe(0);
|
|
293
|
+
// Monday should have interest
|
|
294
|
+
expect(projections[2]?.projectedDailyInterest).toBeGreaterThan(0);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('date range helpers', () => {
|
|
299
|
+
it('should get month-to-date range correctly', () => {
|
|
300
|
+
const today = new Date(2025, 0, 15); // January 15, 2025
|
|
301
|
+
const range = getMonthToDateRange(today);
|
|
302
|
+
|
|
303
|
+
expect(range.fromDate).toBe('2025-01-01');
|
|
304
|
+
expect(range.toDate).toBe('2025-01-15');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should get year-to-date range correctly', () => {
|
|
308
|
+
const today = new Date(2025, 5, 15); // June 15, 2025
|
|
309
|
+
const range = getYearToDateRange(today);
|
|
310
|
+
|
|
311
|
+
expect(range.fromDate).toBe('2025-01-01');
|
|
312
|
+
expect(range.toDate).toBe('2025-06-15');
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe('formatDateString and parseDateString', () => {
|
|
317
|
+
it('should format date correctly', () => {
|
|
318
|
+
const date = new Date(2025, 0, 15);
|
|
319
|
+
expect(formatDateString(date)).toBe('2025-01-15');
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('should parse date string correctly', () => {
|
|
323
|
+
const result = parseDateString('2025-01-15');
|
|
324
|
+
expect(result.getFullYear()).toBe(2025);
|
|
325
|
+
expect(result.getMonth()).toBe(0);
|
|
326
|
+
expect(result.getDate()).toBe(15);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should roundtrip correctly', () => {
|
|
330
|
+
const original = new Date(2025, 11, 31);
|
|
331
|
+
const str = formatDateString(original);
|
|
332
|
+
const parsed = parseDateString(str);
|
|
333
|
+
expect(formatDateString(parsed)).toBe(str);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
detectInterestTransaction,
|
|
5
|
+
detectInterestTransactions,
|
|
6
|
+
summarizeDetectionResults,
|
|
7
|
+
type TransactionForDetection,
|
|
8
|
+
} from '../finance/interest-detector';
|
|
9
|
+
|
|
10
|
+
describe('interest-detector', () => {
|
|
11
|
+
describe('detectInterestTransaction', () => {
|
|
12
|
+
it('should detect transaction with Vietnamese interest keyword in description', () => {
|
|
13
|
+
const tx: TransactionForDetection = {
|
|
14
|
+
id: '1',
|
|
15
|
+
amount: 1000,
|
|
16
|
+
description: 'Tiền lãi hàng ngày',
|
|
17
|
+
date: '2025-01-15',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const result = detectInterestTransaction(tx);
|
|
21
|
+
expect(result).not.toBeNull();
|
|
22
|
+
expect(result?.confidence).toBe('high');
|
|
23
|
+
expect(result?.matchReason).toContain(
|
|
24
|
+
'Description matches interest pattern'
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should detect transaction with English interest keyword', () => {
|
|
29
|
+
const tx: TransactionForDetection = {
|
|
30
|
+
id: '1',
|
|
31
|
+
amount: 1000,
|
|
32
|
+
description: 'Daily interest payment',
|
|
33
|
+
date: '2025-01-15',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const result = detectInterestTransaction(tx);
|
|
37
|
+
expect(result).not.toBeNull();
|
|
38
|
+
expect(result?.confidence).toBe('high');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should detect Momo-specific patterns', () => {
|
|
42
|
+
const tx: TransactionForDetection = {
|
|
43
|
+
id: '1',
|
|
44
|
+
amount: 1000,
|
|
45
|
+
description: 'Momo interest reward',
|
|
46
|
+
date: '2025-01-15',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const result = detectInterestTransaction(tx);
|
|
50
|
+
expect(result).not.toBeNull();
|
|
51
|
+
expect(result?.confidence).toBe('high');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should return null for negative amounts (expenses)', () => {
|
|
55
|
+
const tx: TransactionForDetection = {
|
|
56
|
+
id: '1',
|
|
57
|
+
amount: -1000,
|
|
58
|
+
description: 'Tiền lãi',
|
|
59
|
+
date: '2025-01-15',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const result = detectInterestTransaction(tx);
|
|
63
|
+
expect(result).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should return null for zero amounts', () => {
|
|
67
|
+
const tx: TransactionForDetection = {
|
|
68
|
+
id: '1',
|
|
69
|
+
amount: 0,
|
|
70
|
+
description: 'Tiền lãi',
|
|
71
|
+
date: '2025-01-15',
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const result = detectInterestTransaction(tx);
|
|
75
|
+
expect(result).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should return null for unrelated descriptions', () => {
|
|
79
|
+
const tx: TransactionForDetection = {
|
|
80
|
+
id: '1',
|
|
81
|
+
amount: 50000,
|
|
82
|
+
description: 'Coffee purchase',
|
|
83
|
+
date: '2025-01-15',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const result = detectInterestTransaction(tx);
|
|
87
|
+
expect(result).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should have medium confidence when amount matches expected daily interest', () => {
|
|
91
|
+
const tx: TransactionForDetection = {
|
|
92
|
+
id: '1',
|
|
93
|
+
amount: 1000,
|
|
94
|
+
description: null,
|
|
95
|
+
date: '2025-01-15',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// If expected daily interest is 1000, and amount is 1000, should be medium confidence
|
|
99
|
+
const result = detectInterestTransaction(tx, 1000);
|
|
100
|
+
expect(result).not.toBeNull();
|
|
101
|
+
expect(result?.confidence).toBe('medium');
|
|
102
|
+
expect(result?.matchReason).toContain(
|
|
103
|
+
'Amount matches expected daily interest'
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should have high confidence when description matches AND amount matches', () => {
|
|
108
|
+
const tx: TransactionForDetection = {
|
|
109
|
+
id: '1',
|
|
110
|
+
amount: 1000,
|
|
111
|
+
description: 'Tiền lãi',
|
|
112
|
+
date: '2025-01-15',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const result = detectInterestTransaction(tx, 1000);
|
|
116
|
+
expect(result).not.toBeNull();
|
|
117
|
+
expect(result?.confidence).toBe('high');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should handle null description', () => {
|
|
121
|
+
const tx: TransactionForDetection = {
|
|
122
|
+
id: '1',
|
|
123
|
+
amount: 50000,
|
|
124
|
+
description: null,
|
|
125
|
+
date: '2025-01-15',
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Without description match or amount match, should return null
|
|
129
|
+
const result = detectInterestTransaction(tx);
|
|
130
|
+
expect(result).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('detectInterestTransactions', () => {
|
|
135
|
+
it('should detect multiple interest transactions', () => {
|
|
136
|
+
const transactions: TransactionForDetection[] = [
|
|
137
|
+
{ id: '1', amount: 1000, description: 'Tiền lãi', date: '2025-01-15' },
|
|
138
|
+
{ id: '2', amount: -500, description: 'Coffee', date: '2025-01-15' },
|
|
139
|
+
{
|
|
140
|
+
id: '3',
|
|
141
|
+
amount: 1100,
|
|
142
|
+
description: 'Daily interest',
|
|
143
|
+
date: '2025-01-16',
|
|
144
|
+
},
|
|
145
|
+
{ id: '4', amount: 500000, description: 'Salary', date: '2025-01-17' }, // Large amount not matching pattern
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
const results = detectInterestTransactions(transactions);
|
|
149
|
+
expect(results).toHaveLength(2);
|
|
150
|
+
expect(results[0]?.transactionId).toBe('3'); // Sorted by date desc
|
|
151
|
+
expect(results[1]?.transactionId).toBe('1');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should sort results by date descending', () => {
|
|
155
|
+
const transactions: TransactionForDetection[] = [
|
|
156
|
+
{ id: '1', amount: 1000, description: 'Lãi suất', date: '2025-01-10' },
|
|
157
|
+
{ id: '2', amount: 1000, description: 'Lãi suất', date: '2025-01-20' },
|
|
158
|
+
{ id: '3', amount: 1000, description: 'Lãi suất', date: '2025-01-15' },
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
const results = detectInterestTransactions(transactions);
|
|
162
|
+
expect(results[0]?.date).toBe('2025-01-20');
|
|
163
|
+
expect(results[1]?.date).toBe('2025-01-15');
|
|
164
|
+
expect(results[2]?.date).toBe('2025-01-10');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should return empty array for no matches', () => {
|
|
168
|
+
const transactions: TransactionForDetection[] = [
|
|
169
|
+
{ id: '1', amount: -500, description: 'Coffee', date: '2025-01-15' },
|
|
170
|
+
{ id: '2', amount: 500000, description: 'Salary', date: '2025-01-17' }, // Large amount, no pattern match
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
const results = detectInterestTransactions(transactions);
|
|
174
|
+
expect(results).toHaveLength(0);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('summarizeDetectionResults', () => {
|
|
179
|
+
it('should summarize detection results correctly', () => {
|
|
180
|
+
const detected = [
|
|
181
|
+
{
|
|
182
|
+
transactionId: '1',
|
|
183
|
+
date: '2025-01-15',
|
|
184
|
+
amount: 1000,
|
|
185
|
+
description: 'Tiền lãi',
|
|
186
|
+
confidence: 'high' as const,
|
|
187
|
+
matchReason: 'Description matches',
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
transactionId: '2',
|
|
191
|
+
date: '2025-01-16',
|
|
192
|
+
amount: 1100,
|
|
193
|
+
description: '',
|
|
194
|
+
confidence: 'medium' as const,
|
|
195
|
+
matchReason: 'Amount matches',
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
transactionId: '3',
|
|
199
|
+
date: '2025-01-17',
|
|
200
|
+
amount: 500,
|
|
201
|
+
description: '',
|
|
202
|
+
confidence: 'low' as const,
|
|
203
|
+
matchReason: 'Small amount',
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
const summary = summarizeDetectionResults(detected);
|
|
208
|
+
expect(summary.totalAmount).toBe(2600);
|
|
209
|
+
expect(summary.highConfidence).toBe(1);
|
|
210
|
+
expect(summary.mediumConfidence).toBe(1);
|
|
211
|
+
expect(summary.lowConfidence).toBe(1);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should handle empty array', () => {
|
|
215
|
+
const summary = summarizeDetectionResults([]);
|
|
216
|
+
expect(summary.totalAmount).toBe(0);
|
|
217
|
+
expect(summary.highConfidence).toBe(0);
|
|
218
|
+
expect(summary.mediumConfidence).toBe(0);
|
|
219
|
+
expect(summary.lowConfidence).toBe(0);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|