• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kiva / ui / 20721636688

05 Jan 2026 04:17PM UTC coverage: 91.744% (+0.01%) from 91.734%
20721636688

Pull #6559

github

web-flow
Merge cd5d2dea8 into c038e7b15
Pull Request #6559: feat: prevent lent loans to count on goal message

3856 of 4132 branches covered (93.32%)

Branch coverage included in aggregate %.

19480 of 21304 relevant lines covered (91.44%)

77.31 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.16
/src/composables/useGoalData.js
1
import {
1✔
2
        computed,
3
        inject,
4
        ref,
5
} from 'vue';
6

7
import useGoalDataQuery from '#src/graphql/query/useGoalData.graphql';
1✔
8
import useGoalDataProgressQuery from '#src/graphql/query/useGoalDataProgress.graphql';
1✔
9
import useGoalDataYearlyProgressQuery from '#src/graphql/query/useGoalDataYearlyProgress.graphql';
1✔
10
import logFormatter from '#src/util/logFormatter';
1✔
11
import { createUserPreferences, updateUserPreferences } from '#src/util/userPreferenceUtils';
1✔
12

13
import useBadgeData, {
1✔
14
        ID_BASIC_NEEDS,
15
        ID_CLIMATE_ACTION,
16
        ID_REFUGEE_EQUALITY,
17
        ID_SUPPORT_ALL,
18
        ID_US_ECONOMIC_EQUALITY,
19
        ID_WOMENS_EQUALITY,
20
} from '#src/composables/useBadgeData';
21

22
import womenImg from '#src/assets/images/my-kiva/goal-setting/women.svg?url';
1✔
23
import refugeesImg from '#src/assets/images/my-kiva/goal-setting/refugees.svg?url';
1✔
24
import climateActionImg from '#src/assets/images/my-kiva/goal-setting/climate-action.svg?url';
1✔
25
import usEntrepreneursImg from '#src/assets/images/my-kiva/goal-setting/us-entrepreneurs.svg?url';
1✔
26
import basicNeedsImg from '#src/assets/images/my-kiva/goal-setting/basic-needs.svg?url';
1✔
27
import supportAllImg from '#src/assets/images/my-kiva/goal-setting/support-all.svg?url';
1✔
28

29
const GOAL_DISPLAY_MAP = {
1✔
30
        [ID_BASIC_NEEDS]: 'basic needs loans',
1✔
31
        [ID_CLIMATE_ACTION]: 'eco friendly loans',
1✔
32
        [ID_REFUGEE_EQUALITY]: 'refugees',
1✔
33
        [ID_SUPPORT_ALL]: 'borrowers',
1✔
34
        [ID_US_ECONOMIC_EQUALITY]: 'US entrepreneurs',
1✔
35
        [ID_WOMENS_EQUALITY]: 'women',
1✔
36
};
1✔
37

38
const GOAL_1_DISPLAY_MAP = {
1✔
39
        [ID_BASIC_NEEDS]: 'basic needs loan',
1✔
40
        [ID_CLIMATE_ACTION]: 'eco friendly loan',
1✔
41
        [ID_REFUGEE_EQUALITY]: 'refugee',
1✔
42
        [ID_SUPPORT_ALL]: 'borrower',
1✔
43
        [ID_US_ECONOMIC_EQUALITY]: 'US entrepreneur',
1✔
44
        [ID_WOMENS_EQUALITY]: 'woman',
1✔
45
};
1✔
46

47
export const GOAL_STATUS = {
1✔
48
        COMPLETED: 'completed',
1✔
49
        EXPIRED: 'expired',
1✔
50
        IN_PROGRESS: 'in-progress',
1✔
51
};
1✔
52

53
export const SAME_AS_LAST_YEAR_LIMIT = 1;
1✔
54
export const LAST_YEAR_KEY = new Date().getFullYear() - 1;
1✔
55
export const GOALS_V2_START_YEAR = 2026;
1✔
56
export const COMPLETED_GOAL_THRESHOLD = 100;
1✔
57
export const HALF_GOAL_THRESHOLD = 50;
1✔
58

59
/**
1✔
60
 * Check if Goals V2 should be enabled based on the flag or current year
1✔
61
 * Goals V2 is enabled if the flag is true OR the year is 2026 or later
1✔
62
 * @param {boolean} flagEnabled - The thankyou_page_goals_enable flag value
1✔
63
 * @returns {boolean} True if Goals V2 should be enabled
1✔
64
 */
1✔
65
export function isGoalsV2Enabled(flagEnabled) {
1✔
66
        const currentYear = new Date().getFullYear();
7✔
67
        return flagEnabled || currentYear >= GOALS_V2_START_YEAR;
7✔
68
}
7✔
69

70
function getGoalDisplayName(target, category) {
24✔
71
        if (!target || target > 1) return GOAL_DISPLAY_MAP[category] || 'loans';
24✔
72
        return GOAL_1_DISPLAY_MAP[category] || 'loan';
24✔
73
}
24✔
74

75
/**
1✔
76
 * Vue composable for loading and managing user goal data
1✔
77
 *
1✔
78
 * @param {Object} options - Configuration options
1✔
79
 * @param {Array} options.loans - List of loans to count toward goals
1✔
80
 * @param {Object} options.apollo - Apollo client instance (optional, will use inject if not provided)
1✔
81
 * @returns Goal data and utilities
1✔
82
 */
1✔
83
export default function useGoalData({ apollo } = {}) {
1✔
84
        const apolloClient = apollo || inject('apollo');
75!
85
        const $kvTrackEvent = inject('$kvTrackEvent');
75✔
86
        const currentYearProgress = ref([]);
75✔
87
        const goalCurrentLoanCount = ref(0); // In-page counter for tracking loans added to basket
75✔
88
        const loading = ref(true);
75✔
89
        const totalLoanCount = ref(null);
75✔
90
        const userGoal = ref(null);
75✔
91
        const userGoalAchievedNow = ref(false);
75✔
92
        const userPreferences = ref(null);
75✔
93
        const useYearlyProgress = ref(false); // Default to all-time progress (flag disabled behavior)
75✔
94

95
        // --- Computed Properties ---
75✔
96

97
        const goalProgress = computed(() => {
75✔
98
                const goal = userGoal.value;
10✔
99
                const progress = currentYearProgress.value;
10✔
100
                // When flag is enabled (useYearlyProgress = true), use yearly progress
10✔
101
                // When flag is disabled (useYearlyProgress = false), use all-time progress minus loanTotalAtStart
10✔
102
                if (goal?.category === ID_SUPPORT_ALL) {
10✔
103
                        if (useYearlyProgress.value) {
6!
104
                                return totalLoanCount.value || 0;
×
105
                        }
×
106
                        const loanTotalAtStart = goal?.loanTotalAtStart || 0;
6✔
107
                        return Math.max(0, (totalLoanCount.value || 0) - loanTotalAtStart);
6!
108
                }
6✔
109
                const categoryProgress = progress?.find(n => n.id === goal?.category);
10✔
110
                if (useYearlyProgress.value) {
10✔
111
                        return categoryProgress?.progressForYear || 0;
2!
112
                }
2✔
113
                const allTimeProgress = categoryProgress?.totalProgressToAchievement || 0;
10✔
114
                const loanTotalAtStart = goal?.loanTotalAtStart || 0;
10✔
115
                return Math.max(0, allTimeProgress - loanTotalAtStart);
10✔
116
        });
75✔
117

118
        const userGoalAchieved = computed(() => goalProgress.value >= userGoal.value?.target);
75✔
119

120
        /**
75✔
121
         * Check if the current progress would complete the user's goal
75✔
122
         * Used to show "reaches your goal" message in basket and ATB modal
75✔
123
         * @param {number} currentProgress - Current progress toward goal (after adding loan to basket)
75✔
124
         * @returns {boolean} True if this progress completes the goal
75✔
125
         */
75✔
126
        function isProgressCompletingGoal(currentProgress) {
75✔
127
                const goal = userGoal.value;
5✔
128
                if (!goal || goal.status !== GOAL_STATUS.IN_PROGRESS) return false;
5✔
129

130
                const target = goal.target || 0;
5✔
131
                // Check if progress > 0 and equals target (completing the goal)
5✔
132
                return currentProgress > 0 && currentProgress === target;
5✔
133
        }
5✔
134

135
        // --- Functions ---
75✔
136

137
        function setGoalState(parsedPrefs) {
75✔
138
                if (!parsedPrefs) return;
40✔
139
                const goals = parsedPrefs.goals || [];
40✔
140
                const currentYear = new Date().getFullYear();
40✔
141
                const activeGoals = goals.filter(g => {
40✔
142
                        // Filter out expired goals
35✔
143
                        if (g.status === GOAL_STATUS.EXPIRED) return false;
35✔
144
                        // Filter out goals completed in previous years
31✔
145
                        if (g.status === GOAL_STATUS.COMPLETED && g.dateStarted) {
35✔
146
                                const goalYear = new Date(g.dateStarted).getFullYear();
5✔
147
                                if (goalYear < currentYear) return false;
5✔
148
                        }
5✔
149
                        return true;
29✔
150
                });
40✔
151
                userGoal.value = { ...activeGoals[0] };
40✔
152
        }
40✔
153

154
        /**
75✔
155
         * Get Goal Categories for Goal Selection
75✔
156
         * @param {*} categoriesLoanCount Categories Loan Count
75✔
157
         * @param {*} totalLoans Total Loans
75✔
158
         * @returns array of goal categories
75✔
159
         */
75✔
160
        function getCategories(categoriesLoanCount, totalLoans) {
75✔
161
                return [
3✔
162
                        {
3✔
163
                                id: '1',
3✔
164
                                name: 'Women',
3✔
165
                                description: 'Open doors for women around the world',
3✔
166
                                eventProp: 'women',
3✔
167
                                customImage: womenImg,
3✔
168
                                loanCount: categoriesLoanCount?.[ID_WOMENS_EQUALITY],
3✔
169
                                title: 'women',
3✔
170
                                badgeId: ID_WOMENS_EQUALITY,
3✔
171
                        },
3✔
172
                        {
3✔
173
                                id: '2',
3✔
174
                                name: 'Refugees',
3✔
175
                                description: 'Transform the future for refugees',
3✔
176
                                eventProp: 'refugees',
3✔
177
                                customImage: refugeesImg,
3✔
178
                                loanCount: categoriesLoanCount?.[ID_REFUGEE_EQUALITY],
3✔
179
                                title: 'refugees',
3✔
180
                                badgeId: ID_REFUGEE_EQUALITY,
3✔
181
                        },
3✔
182
                        {
3✔
183
                                id: '3',
3✔
184
                                name: 'Climate Action',
3✔
185
                                description: 'Support the front lines of the climate crisis',
3✔
186
                                eventProp: 'climate',
3✔
187
                                customImage: climateActionImg,
3✔
188
                                loanCount: categoriesLoanCount?.[ID_CLIMATE_ACTION],
3✔
189
                                title: 'climate action',
3✔
190
                                badgeId: ID_CLIMATE_ACTION,
3✔
191
                        },
3✔
192
                        {
3✔
193
                                id: '4',
3✔
194
                                name: 'U.S. Entrepreneurs',
3✔
195
                                description: 'Support small businesses in the U.S.',
3✔
196
                                eventProp: 'us-entrepreneur',
3✔
197
                                customImage: usEntrepreneursImg,
3✔
198
                                loanCount: categoriesLoanCount?.[ID_US_ECONOMIC_EQUALITY],
3✔
199
                                title: 'US entrepreneurs',
3✔
200
                                badgeId: ID_US_ECONOMIC_EQUALITY,
3✔
201
                        },
3✔
202
                        {
3✔
203
                                id: '5',
3✔
204
                                name: 'Basic Needs',
3✔
205
                                description: 'Clean water, healthcare, and sanitation',
3✔
206
                                eventProp: 'basic-needs',
3✔
207
                                customImage: basicNeedsImg,
3✔
208
                                loanCount: categoriesLoanCount?.[ID_BASIC_NEEDS],
3✔
209
                                title: 'basic needs',
3✔
210
                                badgeId: ID_BASIC_NEEDS,
3✔
211
                        },
3✔
212
                        {
3✔
213
                                id: '6',
3✔
214
                                name: 'Choose as I go',
3✔
215
                                description: 'Support a variety of borrowers',
3✔
216
                                eventProp: 'help-everyone',
3✔
217
                                customImage: supportAllImg,
3✔
218
                                loanCount: totalLoans,
3✔
219
                                title: null,
3✔
220
                                badgeId: ID_SUPPORT_ALL,
3✔
221
                        }
3✔
222
                ];
3✔
223
        }
3✔
224

225
        /**
75✔
226
         * Generate CTA Href for Goal Completion
75✔
227
         * @param {number} selectedGoalNumber goal target number selected by the user
75✔
228
         * @param {string} categoryId category id selected by the user
75✔
229
         * @param {object} router vue-router instance
75✔
230
         * @param {number} currentLoanCount loans made toward this goal (default 0 for new goals,
75✔
231
         *   pass goalProgress for existing goals to show remaining loans needed)
75✔
232
         * @returns {string} href string with encoded header message
75✔
233
         */
75✔
234
        function getCtaHref(selectedGoalNumber, categoryId, router, currentLoanCount = 0) {
75✔
235
                const { getLoanFindingUrl } = useBadgeData();
8✔
236
                const remaining = Math.max(0, selectedGoalNumber - currentLoanCount);
8✔
237
                const categoryHeader = getGoalDisplayName(remaining, categoryId);
8✔
238
                const string = `Support ${remaining} more ${categoryHeader} to reach your goal`;
8✔
239
                const encodedHeader = encodeURIComponent(string);
8✔
240
                const loanFindingUrl = getLoanFindingUrl(categoryId, router.currentRoute.value);
8✔
241
                return `${loanFindingUrl}?header=${encodedHeader}`;
8✔
242
        }
8✔
243

244
        /**
75✔
245
         * Get the number of loans from last year by the given category ID
75✔
246
         */
75✔
247
        function getCategoryLoansLastYear(tieredAchievements, categoryId = ID_WOMENS_EQUALITY) {
75✔
248
                const categoryAchievement = tieredAchievements?.find(entry => entry.id === categoryId);
5✔
249
                return categoryAchievement?.progressForYear || 0;
5✔
250
        }
5✔
251

252
        /**
75✔
253
         * Retrieves the user's tiered lending achievement progress for a given year.
75✔
254
         *
75✔
255
         * @param {number} year - Year to fetch progress for.
75✔
256
         * @param {string} [fetchPolicy='cache-first'] - Apollo fetch policy.
75✔
257
         * @returns {Promise<Object[]|null>} Tiered lending progress data, or null on error.
75✔
258
         */
75✔
259
        async function getCategoriesProgressByYear(year, fetchPolicy = 'cache-first') {
75✔
260
                try {
43✔
261
                        const response = await apolloClient.query({
43✔
262
                                query: useGoalDataYearlyProgressQuery,
43✔
263
                                variables: { year },
43✔
264
                                fetchPolicy
43✔
265
                        });
43✔
266
                        const progress = response.data.userAchievementProgress.tieredLendingAchievements;
40✔
267
                        return progress;
40✔
268
                } catch (error) {
43✔
269
                        logFormatter(error, 'Failed to fetch categories progress by year');
4✔
270
                        return null;
4✔
271
                }
4✔
272
        }
43✔
273

274
        /**
75✔
275
         * Retrieves the user's loan count for the specified category id and year.
75✔
276
         *
75✔
277
         * @param {string} categoryId - Category ID to fetch loan count for.
75✔
278
         * @param {number} year - Year to fetch progress for.
75✔
279
         * @param {string} [fetchPolicy='cache-first'] - Apollo fetch policy.
75✔
280
         * @returns {number|null} The category loan count for the given year, or null on error.
75✔
281
         */
75✔
282
        async function getCategoryLoanCountByYear(categoryId, year, fetchPolicy = 'cache-first') {
75✔
283
                try {
6✔
284
                        const progress = await getCategoriesProgressByYear(year, fetchPolicy);
6✔
285
                        const count = progress?.find(entry => entry.id === categoryId)?.progressForYear || 0;
6✔
286
                        return count;
6✔
287
                } catch (error) {
6!
288
                        logFormatter(error, 'Failed to fetch category loan count by year');
×
289
                        return null;
×
290
                }
×
291
        }
6✔
292

293
        async function loadPreferences(fetchPolicy = 'cache-first') {
75✔
294
                try {
45✔
295
                        const response = await apolloClient.query({ query: useGoalDataQuery, fetchPolicy });
45✔
296
                        const prefsData = response.data?.my?.userPreferences || null;
45✔
297
                        totalLoanCount.value = response.data?.my?.loans?.totalCount || 0;
45✔
298
                        userPreferences.value = prefsData;
45✔
299
                        return prefsData ? JSON.parse(prefsData.preferences || '{}') : {};
45!
300
                } catch (error) {
45✔
301
                        logFormatter(error, 'Failed to load preferences');
2✔
302
                        return null;
2✔
303
                }
2✔
304
        }
45✔
305

306
        async function loadProgress(year, fetchPolicy = 'network-only') {
75✔
307
                try {
33✔
308
                        const progress = await getCategoriesProgressByYear(year, fetchPolicy);
33✔
309
                        currentYearProgress.value = progress;
33✔
310
                } catch (error) {
33!
311
                        logFormatter(error, 'Failed to load progress');
×
312
                        return null;
×
313
                }
×
314
        }
33✔
315

316
        /**
75✔
317
         * Get post-checkout progress for multiple loans
75✔
318
         * @param {Object} options - Options for progress calculation
75✔
319
         * @param {Array} options.loans - Array of loan objects with id property
75✔
320
         * @param {number|null} options.year - Year for yearly progress, or null for all-time progress
75✔
321
         * @param {boolean} options.increment - Increment in-page counter by 1 (ATB modal: true)
75✔
322
         * @param {boolean} options.addBasketLoans - Add loans.length to progress (Basket page: true)
75✔
323
         * @returns {number} Progress count for the user's goal category
75✔
324
         *
75✔
325
         * Use cases:
75✔
326
         * - ATB Modal: { loans, increment: true } - increments counter per add-to-basket action
75✔
327
         * - Basket Page: { loans, addBasketLoans: true } - adds basket loan count
75✔
328
         * - Thanks Page: { loans, year } - returns goalProgress (loans already in totalLoanCount)
75✔
329
         */
75✔
330
        async function getPostCheckoutProgressByLoans({
75✔
331
                loans = [],
6✔
332
                year = null,
6✔
333
                increment = false,
6✔
334
                addBasketLoans = false,
6✔
335
        } = {}) {
6✔
336
                // For ID_SUPPORT_ALL, use in-page counter logic instead of API query
6✔
337
                // goalProgress already accounts for loanTotalAtStart
6✔
338
                if (userGoal.value?.category === ID_SUPPORT_ALL) {
6✔
339
                        if (increment) {
4✔
340
                                // ATB modal: increment by 1 per add-to-basket action
2✔
341
                                goalCurrentLoanCount.value += 1;
2✔
342
                                return goalProgress.value + goalCurrentLoanCount.value;
2✔
343
                        }
2✔
344
                        if (addBasketLoans) {
4✔
345
                                // Basket page: add basket loan count (loans not yet in totalLoanCount)
1✔
346
                                return goalProgress.value + loans.length;
1✔
347
                        }
1✔
348
                        // Thanks page: just return goalProgress (loans already in totalLoanCount after checkout)
1✔
349
                        return goalProgress.value;
1✔
350
                }
1✔
351
                try {
2✔
352
                        const loanIds = loans.map(loan => loan.id);
2✔
353
                        const response = await apolloClient.query({
2✔
354
                                query: useGoalDataProgressQuery,
2✔
355
                                variables: { loanIds, year },
2✔
356
                        });
2✔
357
                        // Use allTimeProgress when year is null/undefined, otherwise use yearlyProgress
2✔
358
                        const progress = year
2✔
359
                                ? response.data?.postCheckoutAchievements?.yearlyProgress || []
6!
360
                                : response.data?.postCheckoutAchievements?.allTimeProgress || [];
6✔
361
                        const totalProgress = progress.find(
6✔
362
                                entry => entry.achievementId === userGoal.value?.category
6✔
363
                        )?.totalProgress || 0;
6✔
364
                        // When using all-time progress, subtract loanTotalAtStart to get progress since goal was set
6✔
365
                        if (!year) {
6✔
366
                                const loanTotalAtStart = userGoal.value?.loanTotalAtStart || 0;
2✔
367
                                return Math.max(0, totalProgress - loanTotalAtStart);
2✔
368
                        }
2!
369
                        return totalProgress;
×
370
                } catch (error) {
×
371
                        logFormatter(error, 'Failed to get post-checkout progress');
×
372
                        return null;
×
373
                }
×
374
        }
6✔
375

376
        async function storeGoalPreferences(updates) {
75✔
377
                if (!userPreferences.value?.id) {
5✔
378
                        await createUserPreferences(apolloClient, { goals: [] });
2✔
379
                        await loadPreferences('network-only'); // Reload after create
2✔
380
                }
2✔
381
                const parsedPrefs = JSON.parse(userPreferences.value?.preferences || '{}');
5✔
382
                const goals = parsedPrefs.goals || [];
5✔
383
                const goalIndex = goals.findIndex(g => g.goalName === updates.goalName);
5✔
384
                if (goalIndex !== -1) {
5✔
385
                        goals[goalIndex] = { ...goals[goalIndex], ...updates };
3✔
386
                } else {
5✔
387
                        // When creating a new goal, set loanTotalAtStart to current all-time progress for the category
2✔
388
                        // This allows tracking progress from the point the goal was set
2✔
389
                        // For ID_SUPPORT_ALL, use totalLoanCount since it tracks total loans, not category-specific progress
2✔
390
                        let loanTotalAtStart;
2✔
391
                        if (updates.category === ID_SUPPORT_ALL) {
2!
392
                                loanTotalAtStart = totalLoanCount.value || 0;
×
393
                        } else {
2✔
394
                                const categoryProgress = currentYearProgress.value?.find(n => n.id === updates.category);
2✔
395
                                loanTotalAtStart = categoryProgress?.totalProgressToAchievement || 0;
2✔
396
                        }
2✔
397
                        goals.push({ ...updates, loanTotalAtStart });
2✔
398
                }
2✔
399
                await updateUserPreferences(
5✔
400
                        apolloClient,
5✔
401
                        userPreferences.value,
5✔
402
                        parsedPrefs,
5✔
403
                        { goals }
5✔
404
                );
5✔
405
                setGoalState({ goals }); // Refresh local state after update
5✔
406
        }
5✔
407

408
        async function checkCompletedGoal({ currentGoalProgress = 0, category = 'post-checkout' } = {}) {
75✔
409
                // Skip if goal is already completed or expired
4✔
410
                if ([GOAL_STATUS.COMPLETED, GOAL_STATUS.EXPIRED].includes(userGoal.value?.status)) {
4✔
411
                        return;
1✔
412
                }
1✔
413
                if (
3✔
414
                        (currentGoalProgress && (currentGoalProgress >= userGoal.value?.target))
4✔
415
                        || (userGoal.value && userGoalAchieved.value)
4✔
416
                ) {
4✔
417
                        // Capture goal data before storeGoalPreferences (which may filter out the goal via setGoalState)
2✔
418
                        const goalCategory = userGoal.value.category;
2✔
419
                        const goalTarget = userGoal.value.target;
2✔
420
                        userGoal.value = {
2✔
421
                                ...userGoal.value,
2✔
422
                                status: GOAL_STATUS.COMPLETED
2✔
423
                        };
2✔
424
                        await storeGoalPreferences({ ...userGoal.value });
2✔
425
                        $kvTrackEvent(
2✔
426
                                category,
2✔
427
                                'show',
2✔
428
                                'annual-goal-complete',
2✔
429
                                goalCategory,
2✔
430
                                goalTarget
2✔
431
                        );
2✔
432
                        userGoalAchievedNow.value = true;
2✔
433
                }
2✔
434
        }
4✔
435

436
        /**
75✔
437
         * Check and correct negative goal progress
75✔
438
         * This could happen due to a race condition with postCheckoutAchievements
75✔
439
         * Only applies when yearlyProgress is false (all-time progress mode)
75✔
440
         */
75✔
441
        async function correctNegativeProgress() {
75✔
442
                if (useYearlyProgress.value || !userGoal.value || !currentYearProgress?.value?.length) return;
33✔
443

444
                const goal = userGoal.value;
11✔
445
                if (goal.category === ID_SUPPORT_ALL) return;
33✔
446

447
                const categoryProgress = currentYearProgress.value?.find(n => n.id === goal.category);
33✔
448
                const allTimeProgress = categoryProgress?.totalProgressToAchievement || 0;
33✔
449
                const loanTotalAtStart = goal.loanTotalAtStart || 0;
33✔
450
                const adjustedProgress = allTimeProgress - loanTotalAtStart;
33✔
451

452
                if (adjustedProgress < 0) {
33!
453
                        const debugData = {
×
454
                                category: goal.category,
×
455
                                goalName: goal.goalName,
×
456
                                allTimeProgress,
×
457
                                loanTotalAtStart,
×
458
                                adjustedProgress,
×
459
                                target: goal.target,
×
460
                        };
×
461

462
                        logFormatter('Negative goal progress detected, correcting loanTotalAtStart', 'warn', debugData);
×
463

464
                        // Correct the goal by updating loanTotalAtStart to allTimeProgress
×
465
                        await storeGoalPreferences({
×
466
                                ...goal,
×
467
                                loanTotalAtStart: allTimeProgress,
×
468
                        });
×
469
                }
×
470
        }
33✔
471

472
        async function loadGoalData({
75✔
473
                loans = [], // Loans already in basket, used to initialize in-page counter for ID_SUPPORT_ALL
33✔
474
                year = new Date().getFullYear(),
33✔
475
                yearlyProgress = false, // thankyou_page_goals_enable flag - when true uses yearly, when false uses all-time
33✔
476
        } = {}) {
33✔
477
                loading.value = true;
33✔
478
                useYearlyProgress.value = yearlyProgress;
33✔
479
                const parsedPrefs = await loadPreferences();
33✔
480
                await loadProgress(year);
33✔
481
                setGoalState(parsedPrefs);
33✔
482
                // Initialize in-page counter for ID_SUPPORT_ALL based on loans already in basket
33✔
483
                if (userGoal.value?.category === ID_SUPPORT_ALL && loans.length > 0 && !goalCurrentLoanCount.value) {
33!
484
                        // Reducing counter by 1 because loans already has the added loan
×
485
                        goalCurrentLoanCount.value = loans.length - 1;
×
486
                }
×
487
                // Check and correct negative progress after loading
33✔
488
                await correctNegativeProgress();
33✔
489
                loading.value = false;
33✔
490
        }
33✔
491

492
        /**
75✔
493
         * This method renew goals annually.
75✔
494
         * It invalidates all goals on Jan 1st of 2026
75✔
495
         * @return {Array} - expiredGoals
75✔
496
         */
75✔
497
        async function renewAnnualGoal(today = new Date()) {
75✔
498
                const parsedPrefs = await loadPreferences();
3✔
499
                const goals = parsedPrefs.goals || [];
3!
500
                const currentYear = today.getFullYear();
3✔
501
                const renewedYear = parsedPrefs.goalsRenewedDate ? new Date(parsedPrefs.goalsRenewedDate).getFullYear() : null;
3✔
502
                const areGoalsRenewed = goals.some(goal => goal.status === GOAL_STATUS.EXPIRED);
3✔
503
                if (renewedYear > currentYear || areGoalsRenewed) {
3!
504
                        return {
×
505
                                expiredGoals: goals,
×
506
                                showRenewedAnnualGoalToast: false,
×
507
                        };
×
508
                }
×
509

510
                // Renew goals every following year
3✔
511
                const expiredGoals = goals.map(goal => {
3✔
512
                        const goalYear = goal.dateStarted ? new Date(goal.dateStarted).getFullYear() : null;
3!
513
                        if (goalYear < currentYear) {
3✔
514
                                return {
2✔
515
                                        ...goal,
2✔
516
                                        status: GOAL_STATUS.EXPIRED
2✔
517
                                };
2✔
518
                        }
2✔
519
                        return null;
1✔
520
                }).filter(goal => goal !== null);
3✔
521

522
                if (expiredGoals.some(goal => goal.status === GOAL_STATUS.EXPIRED)) {
3✔
523
                        parsedPrefs.goals = expiredGoals;
2✔
524
                        parsedPrefs.goalsRenewedDate = today.toISOString();
2✔
525
                        await updateUserPreferences(
2✔
526
                                apolloClient,
2✔
527
                                userPreferences.value,
2✔
528
                                parsedPrefs,
2✔
529
                                { goals: expiredGoals }
2✔
530
                        );
2✔
531
                        setGoalState({ goals: expiredGoals });
2✔
532
                }
2✔
533

534
                const showRenewedAnnualGoalToast = !!expiredGoals.length
3✔
535
                        && !expiredGoals.some(g => g.status === GOAL_STATUS.COMPLETED);
3✔
536

537
                return {
3✔
538
                        expiredGoals,
3✔
539
                        showRenewedAnnualGoalToast,
3✔
540
                };
3✔
541
        }
3✔
542

543
        async function setHideGoalCardPreference(hide = true) {
75✔
544
                const parsedPrefs = await loadPreferences('network-only');
2✔
545
                await updateUserPreferences(
2✔
546
                        apolloClient,
2✔
547
                        userPreferences.value,
2✔
548
                        parsedPrefs,
2✔
549
                        { hideGoalCard: hide }
2✔
550
                );
2✔
551
        }
2✔
552

553
        function hideGoalCard() {
75✔
554
                const parsedPrefs = JSON.parse(userPreferences.value?.preferences || '{}');
2✔
555
                return parsedPrefs.hideGoalCard || false;
2✔
556
        }
2✔
557

558
        return {
75✔
559
                checkCompletedGoal,
75✔
560
                getCategories,
75✔
561
                getCategoriesProgressByYear,
75✔
562
                getCategoryLoanCountByYear,
75✔
563
                getCategoryLoansLastYear,
75✔
564
                getCtaHref,
75✔
565
                getGoalDisplayName,
75✔
566
                getPostCheckoutProgressByLoans,
75✔
567
                goalProgress,
75✔
568
                isProgressCompletingGoal,
75✔
569
                loadGoalData,
75✔
570
                loadPreferences,
75✔
571
                loading,
75✔
572
                storeGoalPreferences,
75✔
573
                userGoal,
75✔
574
                userGoalAchieved,
75✔
575
                userGoalAchievedNow,
75✔
576
                userPreferences,
75✔
577
                // Goal Entry for 2026 Goals
75✔
578
                renewAnnualGoal,
75✔
579
                hideGoalCard,
75✔
580
                setHideGoalCardPreference,
75✔
581
        };
75✔
582
}
75✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc