@tuturuuu/utils 0.0.2 → 0.6.0

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 (186) hide show
  1. package/CHANGELOG.md +305 -0
  2. package/biome.json +5 -0
  3. package/jsr.json +8 -8
  4. package/package.json +63 -32
  5. package/src/__tests__/ai-temp-auth.test.ts +309 -0
  6. package/src/__tests__/api-proxy-guard.test.ts +1451 -0
  7. package/src/__tests__/app-url.test.ts +270 -0
  8. package/src/__tests__/avatar-url.test.ts +97 -0
  9. package/src/__tests__/color-helper.test.ts +179 -0
  10. package/src/__tests__/constants.test.ts +351 -0
  11. package/src/__tests__/crypto.test.ts +107 -0
  12. package/src/__tests__/date-helper.test.ts +408 -0
  13. package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
  14. package/src/__tests__/format.test.ts +317 -0
  15. package/src/__tests__/html-sanitizer.test.ts +360 -0
  16. package/src/__tests__/interest-calculator.test.ts +336 -0
  17. package/src/__tests__/interest-detector.test.ts +222 -0
  18. package/src/__tests__/label-colors.test.ts +241 -0
  19. package/src/__tests__/name-helper.test.ts +158 -0
  20. package/src/__tests__/node-diff.test.ts +576 -0
  21. package/src/__tests__/notification-service.test.ts +210 -0
  22. package/src/__tests__/onboarding-helper.test.ts +331 -0
  23. package/src/__tests__/path-helper.test.ts +152 -0
  24. package/src/__tests__/permissions.test.tsx +81 -0
  25. package/src/__tests__/request-emoji-limit.test.ts +172 -0
  26. package/src/__tests__/search-helper.test.ts +51 -0
  27. package/src/__tests__/storage-display-name.test.ts +37 -0
  28. package/src/__tests__/storage-path.test.ts +238 -0
  29. package/src/__tests__/tag-utils.test.ts +205 -0
  30. package/src/__tests__/task-description-yjs-state.test.ts +581 -0
  31. package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
  32. package/src/__tests__/task-helper-create-task.test.ts +129 -0
  33. package/src/__tests__/task-helpers.test.ts +464 -0
  34. package/src/__tests__/task-overrides.test.ts +305 -0
  35. package/src/__tests__/task-reorder-cache.test.ts +74 -0
  36. package/src/__tests__/task-sort-keys.test.ts +36 -0
  37. package/src/__tests__/task-transformers.test.ts +62 -0
  38. package/src/__tests__/text-helper.test.ts +776 -0
  39. package/src/__tests__/time-helper.test.ts +70 -0
  40. package/src/__tests__/time-tracker-period.test.ts +55 -0
  41. package/src/__tests__/timezone.test.ts +117 -0
  42. package/src/__tests__/upstash-rest.test.ts +77 -0
  43. package/src/__tests__/uuid-helper.test.ts +133 -0
  44. package/src/__tests__/workspace-helper.test.ts +859 -0
  45. package/src/__tests__/workspace-limits.test.ts +255 -0
  46. package/src/__tests__/yjs-helper.test.ts +581 -0
  47. package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
  48. package/src/abuse-protection/__tests__/edge.test.ts +136 -0
  49. package/src/abuse-protection/__tests__/index.test.ts +562 -0
  50. package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
  51. package/src/abuse-protection/backend-rate-limit.ts +44 -0
  52. package/src/abuse-protection/constants.ts +117 -0
  53. package/src/abuse-protection/edge.ts +223 -0
  54. package/src/abuse-protection/index.ts +1545 -0
  55. package/src/abuse-protection/reputation.ts +587 -0
  56. package/src/abuse-protection/types.ts +97 -0
  57. package/src/abuse-protection/user-agent.ts +124 -0
  58. package/src/abuse-protection/user-suspension.ts +231 -0
  59. package/src/ai-temp-auth.ts +315 -0
  60. package/src/api-proxy-guard.ts +965 -0
  61. package/src/app-url.ts +96 -0
  62. package/src/avatar-url.ts +64 -0
  63. package/src/break-duration.ts +84 -0
  64. package/src/calendar-auth-token.test.ts +37 -0
  65. package/src/calendar-auth-token.ts +19 -0
  66. package/src/calendar-sync-coordination.md +197 -0
  67. package/src/calendar-utils.test.ts +169 -0
  68. package/src/calendar-utils.ts +91 -0
  69. package/src/color-helper.ts +110 -0
  70. package/src/common/nextjs.tsx +99 -0
  71. package/src/common/scan.tsx +15 -0
  72. package/src/configs/reports.ts +160 -0
  73. package/src/constants.ts +85 -0
  74. package/src/crypto.ts +21 -0
  75. package/src/currencies.ts +97 -0
  76. package/src/date-helper.ts +313 -0
  77. package/src/editor/convert-to-task.ts +264 -0
  78. package/src/editor/index.ts +5 -0
  79. package/src/email/__tests__/client.test.ts +141 -0
  80. package/src/email/__tests__/validation.test.ts +46 -0
  81. package/src/email/client.ts +92 -0
  82. package/src/email/server.ts +128 -0
  83. package/src/email/validation.ts +11 -0
  84. package/src/encryption/__tests__/calendar-events.test.ts +411 -0
  85. package/src/encryption/__tests__/configuration.test.ts +114 -0
  86. package/src/encryption/__tests__/field-encryption.test.ts +232 -0
  87. package/src/encryption/__tests__/key-generation.test.ts +30 -0
  88. package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
  89. package/src/encryption/__tests__/test-helpers.ts +22 -0
  90. package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
  91. package/src/encryption/encryption-service.ts +343 -0
  92. package/src/encryption/index.ts +25 -0
  93. package/src/encryption/types.ts +57 -0
  94. package/src/exchange-rates.ts +49 -0
  95. package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
  96. package/src/feature-flags/core.ts +322 -0
  97. package/src/feature-flags/data.ts +16 -0
  98. package/src/feature-flags/default.ts +18 -0
  99. package/src/feature-flags/index.ts +7 -0
  100. package/src/feature-flags/requestable-features.ts +79 -0
  101. package/src/feature-flags/types.ts +4 -0
  102. package/src/fetcher.ts +2 -0
  103. package/src/finance/index.ts +4 -0
  104. package/src/finance/interest-calculator.ts +456 -0
  105. package/src/finance/interest-detector.ts +141 -0
  106. package/src/finance/transform-invoice-results.ts +219 -0
  107. package/src/finance/wallet-permissions.test.ts +169 -0
  108. package/src/finance/wallet-permissions.ts +82 -0
  109. package/src/format.ts +122 -3
  110. package/src/generated/platform-build-metadata.ts +11 -0
  111. package/src/hooks/use-platform.ts +64 -0
  112. package/src/html-sanitizer.ts +155 -0
  113. package/src/internal-domains.ts +497 -0
  114. package/src/keyboard-preset.ts +109 -0
  115. package/src/label-colors.ts +213 -0
  116. package/src/launchable-apps.test.ts +126 -0
  117. package/src/launchable-apps.ts +490 -0
  118. package/src/name-helper.ts +269 -0
  119. package/src/next-config.test.ts +234 -0
  120. package/src/next-config.ts +203 -0
  121. package/src/node-diff.ts +375 -0
  122. package/src/notification-service.ts +379 -0
  123. package/src/nova/scores/__tests__/calculate.test.ts +254 -0
  124. package/src/nova/scores/calculate.ts +132 -0
  125. package/src/nova/submissions/check-permission.ts +132 -0
  126. package/src/onboarding-helper.ts +213 -0
  127. package/src/path-helper.ts +93 -0
  128. package/src/permissions.tsx +1170 -0
  129. package/src/plan-helpers.test.ts +188 -0
  130. package/src/plan-helpers.ts +80 -0
  131. package/src/platform-release.test.ts +74 -0
  132. package/src/platform-release.ts +155 -0
  133. package/src/portless.ts +124 -0
  134. package/src/priority-styles.ts +42 -0
  135. package/src/request-emoji-limit.ts +335 -0
  136. package/src/search-helper.ts +18 -0
  137. package/src/search.test.ts +89 -0
  138. package/src/search.ts +355 -0
  139. package/src/storage-display-name.ts +30 -0
  140. package/src/storage-path.ts +147 -0
  141. package/src/tag-utils.ts +159 -0
  142. package/src/task/reorder.ts +245 -0
  143. package/src/task/transformers.ts +149 -0
  144. package/src/task-date-timezone.ts +133 -0
  145. package/src/task-description-content.ts +240 -0
  146. package/src/task-helper/board.ts +193 -0
  147. package/src/task-helper/bulk-actions.ts +564 -0
  148. package/src/task-helper/personal-external-staging.ts +21 -0
  149. package/src/task-helper/recycle-bin.ts +202 -0
  150. package/src/task-helper/relationships.ts +346 -0
  151. package/src/task-helper/shared.ts +109 -0
  152. package/src/task-helper/sort-keys.ts +337 -0
  153. package/src/task-helper/task-hooks-basic.ts +342 -0
  154. package/src/task-helper/task-hooks-move.ts +264 -0
  155. package/src/task-helper/task-operations.ts +278 -0
  156. package/src/task-helper.ts +12 -0
  157. package/src/task-helpers.ts +241 -0
  158. package/src/task-list-status.ts +62 -0
  159. package/src/task-overrides.ts +82 -0
  160. package/src/task-snapshot.ts +374 -0
  161. package/src/text-diff.ts +81 -0
  162. package/src/text-helper.ts +537 -0
  163. package/src/time-helper.ts +63 -0
  164. package/src/time-tracker-period.ts +73 -0
  165. package/src/timeblock-helper.ts +418 -0
  166. package/src/timezone.ts +190 -0
  167. package/src/timezones.json +1271 -0
  168. package/src/upstash-rest.ts +56 -0
  169. package/src/user-helper.ts +296 -0
  170. package/src/uuid-helper.ts +11 -0
  171. package/src/workspace-handle.ts +10 -0
  172. package/src/workspace-helper.ts +1408 -0
  173. package/src/workspace-limits.ts +68 -0
  174. package/src/yjs-helper.ts +217 -0
  175. package/src/yjs-task-description.ts +81 -0
  176. package/tsconfig.json +3 -5
  177. package/tsconfig.typecheck.json +33 -0
  178. package/vitest.config.ts +36 -0
  179. package/dist/index.d.ts +0 -8
  180. package/dist/index.js +0 -2
  181. package/dist/index.js.map +0 -1
  182. package/dist/index.mjs +0 -2
  183. package/dist/index.mjs.map +0 -1
  184. package/eslint.config.mjs +0 -20
  185. package/rollup.config.js +0 -41
  186. 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
+ });