@@ -3,6 +3,7 @@ import { DateTime, Interval } from 'luxon';
3
3
import { makeMintApiRequest } from '@root/src/shared/lib/auth' ;
4
4
import {
5
5
DATE_FILTER_ALL_TIME ,
6
+ MINT_DAILY_TRENDS_MAX_DAYS ,
6
7
MINT_HEADERS ,
7
8
MINT_RATE_LIMIT_DELAY_MS ,
8
9
} from '@root/src/shared/lib/constants' ;
@@ -75,10 +76,40 @@ export const fetchMonthlyBalancesForAccount = async ({
75
76
}
76
77
} ;
77
78
79
+ export const calculateIntervalForAccountHistory = ( monthlyBalances : TrendEntry [ ] ) => {
80
+ const startDate = monthlyBalances [ 0 ] ?. date ;
81
+
82
+ if ( ! startDate ) {
83
+ throw new Error ( 'Unable to determine start date for account history.' ) ;
84
+ }
85
+
86
+ // find the last month with a non-zero balance
87
+ let endDate : string ;
88
+ let monthIndex = monthlyBalances . length - 1 ;
89
+ while ( monthIndex > 0 && monthlyBalances [ monthIndex ] . amount === 0 ) {
90
+ monthIndex -= 1 ;
91
+ endDate = monthlyBalances [ monthIndex ] . date ;
92
+ }
93
+
94
+ const now = DateTime . now ( ) ;
95
+ const approximateRangeEnd = endDate
96
+ ? // Mint trend months are strange and daily balances may be present after the end of the reported
97
+ // month (anecodotally observed daily balances 10 days into the first month that showed a zero
98
+ // monthly balance).
99
+ DateTime . fromISO ( endDate ) . plus ( { month : 1 } ) . endOf ( 'month' )
100
+ : now ;
101
+
102
+ // then fetch balances for each period in the range
103
+ return Interval . fromDateTimes (
104
+ DateTime . fromISO ( startDate ) . startOf ( 'month' ) ,
105
+ ( approximateRangeEnd < now ? approximateRangeEnd : now ) . endOf ( 'day' ) ,
106
+ ) ;
107
+ } ;
108
+
78
109
/**
79
- * Determine earliest date for which account has balance history, and return monthly intervals from then to now.
110
+ * Determine earliest date for which account has balance history, and return 43 day intervals from then to now.
80
111
*/
81
- const fetchMonthlyIntervalsForAccountHistory = async ( {
112
+ const fetchIntervalsForAccountHistory = async ( {
82
113
accountId,
83
114
overrideApiKey,
84
115
} : {
@@ -95,35 +126,25 @@ const fetchMonthlyIntervalsForAccountHistory = async ({
95
126
}
96
127
97
128
const { balancesByDate : monthlyBalances , reportType } = balanceInfo ;
98
-
99
- const startDate = monthlyBalances [ 0 ] ?. date ;
100
-
101
- if ( ! startDate ) {
102
- throw new Error ( 'Unable to determine start date for account history.' ) ;
103
- }
104
-
105
- // then fetch balances for each month in range, since that's the only timeframe that the API will return a balance for each day
106
- const months = Interval . fromDateTimes (
107
- DateTime . fromISO ( startDate ) . startOf ( 'month' ) ,
108
- DateTime . local ( ) . endOf ( 'month' ) ,
109
- ) . splitBy ( {
110
- months : 1 ,
129
+ const interval = calculateIntervalForAccountHistory ( monthlyBalances ) ;
130
+ const periods = interval . splitBy ( {
131
+ days : MINT_DAILY_TRENDS_MAX_DAYS ,
111
132
} ) as Interval [ ] ;
112
133
113
- return { months , reportType } ;
134
+ return { periods , reportType } ;
114
135
} ;
115
136
116
137
/**
117
138
* Fetch balance history for each month for an account.
118
139
*/
119
- const fetchDailyBalancesForMonthIntervals = async ( {
120
- months ,
140
+ const fetchDailyBalancesForAccount = async ( {
141
+ periods ,
121
142
accountId,
122
143
reportType,
123
144
overrideApiKey,
124
145
onProgress,
125
146
} : {
126
- months : Interval [ ] ;
147
+ periods : Interval [ ] ;
127
148
accountId : string ;
128
149
reportType : string ;
129
150
overrideApiKey ?: string ;
@@ -137,8 +158,8 @@ const fetchDailyBalancesForMonthIntervals = async ({
137
158
count : 0 ,
138
159
} ;
139
160
140
- const dailyBalancesByMonth = await withRateLimit ( { delayMs : MINT_RATE_LIMIT_DELAY_MS } ) (
141
- months . map (
161
+ const dailyBalancesByPeriod = await withRateLimit ( { delayMs : MINT_RATE_LIMIT_DELAY_MS } ) (
162
+ periods . map (
142
163
( { start, end } ) =>
143
164
( ) =>
144
165
withRetry ( ( ) =>
@@ -167,13 +188,13 @@ const fetchDailyBalancesForMonthIntervals = async ({
167
188
)
168
189
. finally ( ( ) => {
169
190
counter . count += 1 ;
170
- onProgress ?.( { complete : counter . count , total : months . length } ) ;
191
+ onProgress ?.( { complete : counter . count , total : periods . length } ) ;
171
192
} ) ,
172
193
) ,
173
194
) ,
174
195
) ;
175
196
176
- const balancesByDate = dailyBalancesByMonth . reduce ( ( acc , balances ) => acc . concat ( balances ) , [ ] ) ;
197
+ const balancesByDate = dailyBalancesByPeriod . reduce ( ( acc , balances ) => acc . concat ( balances ) , [ ] ) ;
177
198
178
199
return balancesByDate ;
179
200
} ;
@@ -187,43 +208,43 @@ export const fetchDailyBalancesForAllAccounts = async ({
187
208
} ) => {
188
209
const accounts = await withRetry ( ( ) => fetchAccounts ( { overrideApiKey } ) ) ;
189
210
190
- // first, fetch the range of months we need to fetch for each account
191
- const accountsWithMonthsToFetch = await Promise . all (
211
+ // first, fetch the range of dates we need to fetch for each account
212
+ const accountsWithPeriodsToFetch = await Promise . all (
192
213
accounts . map ( async ( { id : accountId , name : accountName } ) => {
193
- const { months , reportType } = await withDefaultOnError ( { months : [ ] , reportType : '' } ) (
194
- fetchMonthlyIntervalsForAccountHistory ( {
214
+ const { periods , reportType } = await withDefaultOnError ( { periods : [ ] , reportType : '' } ) (
215
+ fetchIntervalsForAccountHistory ( {
195
216
accountId,
196
217
overrideApiKey,
197
218
} ) ,
198
219
) ;
199
- return { months , reportType, accountId, accountName } ;
220
+ return { periods , reportType, accountId, accountName } ;
200
221
} ) ,
201
222
) ;
202
223
203
- // one per account per month
204
- const totalRequestsToFetch = accountsWithMonthsToFetch . reduce (
205
- ( acc , { months } ) => acc + months . length ,
224
+ // one per account per 43 day period
225
+ const totalRequestsToFetch = accountsWithPeriodsToFetch . reduce (
226
+ ( acc , { periods } ) => acc + periods . length ,
206
227
0 ,
207
228
) ;
208
229
209
230
// fetch one account at a time so we don't hit the rate limit
210
231
const balancesByAccount = await resolveSequential (
211
- accountsWithMonthsToFetch . map (
212
- ( { accountId, accountName, months , reportType } , accountIndex ) =>
232
+ accountsWithPeriodsToFetch . map (
233
+ ( { accountId, accountName, periods , reportType } , accountIndex ) =>
213
234
async ( ) => {
214
235
const balances = await withDefaultOnError < TrendEntry [ ] > ( [ ] ) (
215
- fetchDailyBalancesForMonthIntervals ( {
236
+ fetchDailyBalancesForAccount ( {
216
237
accountId,
217
- months ,
238
+ periods ,
218
239
reportType,
219
240
overrideApiKey,
220
241
onProgress : ( { complete } ) => {
221
242
// this is the progress handler for *each* account, so we need to sum up the results before calling onProgress
222
243
223
- const previousAccounts = accountsWithMonthsToFetch . slice ( 0 , accountIndex ) ;
244
+ const previousAccounts = accountsWithPeriodsToFetch . slice ( 0 , accountIndex ) ;
224
245
// since accounts are fetched sequentially, we can assume that all previous accounts have completed all their requests
225
246
const previousCompletedRequestCount = previousAccounts . reduce (
226
- ( acc , { months } ) => acc + months . length ,
247
+ ( acc , { periods } ) => acc + periods . length ,
227
248
0 ,
228
249
) ;
229
250
const completedRequests = previousCompletedRequestCount + complete ;
@@ -344,11 +365,17 @@ export const fetchAccounts = async ({
344
365
345
366
export const formatBalancesAsCSV = ( balances : TrendEntry [ ] , accountName ?: string ) => {
346
367
const header = [ 'Date' , 'Amount' , accountName && 'Account Name' ] . filter ( Boolean ) ;
347
- const rows = balances . map ( ( { date, amount } ) => [
348
- date ,
349
- amount ,
350
- ...( accountName ? [ accountName ] : [ ] ) ,
351
- ] ) ;
368
+ const maybeAccountColumn : [ string ?] = accountName ? [ accountName ] : [ ] ;
369
+ // remove zero balances from the end of the report leaving just the first row if all are zero
370
+ const rows = balances . reduceRight (
371
+ ( acc , { date, amount } , index ) => {
372
+ if ( acc . length || amount !== 0 || index === 0 ) {
373
+ acc . unshift ( [ date , amount , ...maybeAccountColumn ] ) ;
374
+ }
375
+ return acc ;
376
+ } ,
377
+ [ ] as [ string , number , string ?] [ ] ,
378
+ ) ;
352
379
353
380
return formatCSV ( [ header , ...rows ] ) ;
354
381
} ;
0 commit comments