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

kiva / ui / 20440865490

22 Dec 2025 06:40PM UTC coverage: 91.727% (-0.02%) from 91.747%
20440865490

Pull #6524

github

web-flow
Merge 5c18ce7a7 into 892bb8f2f
Pull Request #6524: fix: mp-2329 / Update Goal Suggestion for User Who Has Already Lent

3845 of 4119 branches covered (93.35%)

Branch coverage included in aggregate %.

38 of 47 new or added lines in 1 file covered. (80.85%)

3 existing lines in 1 file now uncovered.

19449 of 21276 relevant lines covered (91.41%)

77.19 hits per line

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

91.01
/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]: 'loans',
1✔
34
        [ID_US_ECONOMIC_EQUALITY]: 'U.S. 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]: 'loan',
1✔
43
        [ID_US_ECONOMIC_EQUALITY]: 'U.S. 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 = 2025;
1✔
55
export const GOALS_V2_START_YEAR = 2026;
1✔
56

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

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

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

93
        // --- Computed Properties ---
49✔
94

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

116
        const userGoalAchieved = computed(() => goalProgress.value >= userGoal.value?.target);
49✔
117

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

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

133
        // --- Functions ---
49✔
134

135
        function setGoalState(parsedPrefs) {
49✔
136
                if (!parsedPrefs) return;
34✔
137
                const goals = parsedPrefs.goals || [];
34✔
138
                const activeGoals = goals.filter(g => g.status !== GOAL_STATUS.EXPIRED);
34✔
139
                userGoal.value = { ...activeGoals[0] };
34✔
140
        }
34✔
141

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

213
        /**
49✔
214
         * Generate CTA Href for Goal Completion
49✔
215
         * @param {*} selectedGoalNumber goal number selected by the user
49✔
216
         * @param {*} categoryId category id selected by the user
49✔
217
         * @param {*} router router instance
49✔
218
         * @returns href string
49✔
219
         */
49✔
220
        function getCtaHref(selectedGoalNumber, categoryId, router) {
49✔
221
                const { getLoanFindingUrl } = useBadgeData();
4✔
222
                const categoryHeader = getGoalDisplayName(selectedGoalNumber, categoryId);
4✔
223
                const string = `Your goal: Support ${selectedGoalNumber} ${categoryHeader}`;
4✔
224
                const encodedHeader = encodeURIComponent(string);
4✔
225
                const loanFindingUrl = getLoanFindingUrl(categoryId, router.currentRoute.value);
4✔
226
                return `${loanFindingUrl}?header=${encodedHeader}`;
4✔
227
        }
4✔
228

229
        /**
49✔
230
         * Get the number of loans from last year by the given category ID
49✔
231
         */
49✔
232
        function getCategoryLoansLastYear(tieredAchievements, categoryId = ID_WOMENS_EQUALITY) {
49✔
233
                const categoryAchievement = tieredAchievements?.find(entry => entry.id === categoryId);
5✔
234
                return categoryAchievement?.progressForYear || 0;
5✔
235
        }
5✔
236

237
        /**
49✔
238
         * Retrieves the user's tiered lending achievement progress for a given year.
49✔
239
         *
49✔
240
         * @param {number} year - Year to fetch progress for.
49✔
241
         * @param {string} [fetchPolicy='cache-first'] - Apollo fetch policy.
49✔
242
         * @returns {Promise<Object[]|null>} Tiered lending progress data, or null on error.
49✔
243
         */
49✔
244
        async function getCategoriesProgressByYear(year, fetchPolicy = 'cache-first') {
49✔
245
                try {
27✔
246
                        const response = await apolloClient.query({
27✔
247
                                query: useGoalDataYearlyProgressQuery,
27✔
248
                                variables: { year },
27✔
249
                                fetchPolicy
27✔
250
                        });
27✔
251
                        const progress = response.data.userAchievementProgress.tieredLendingAchievements;
26✔
252
                        return progress;
26✔
253
                } catch (error) {
27✔
254
                        logFormatter(error, 'Failed to fetch categories progress by year');
2✔
255
                        return null;
2✔
256
                }
2✔
257
        }
27✔
258

259
        /**
49✔
260
         * Retrieves the user's loan count for the specified category id and year.
49✔
261
         *
49✔
262
         * @param {string} categoryId - Category ID to fetch loan count for.
49✔
263
         * @param {number} year - Year to fetch progress for.
49✔
264
         * @param {string} [fetchPolicy='cache-first'] - Apollo fetch policy.
49✔
265
         * @returns {number|null} The category loan count for the given year, or null on error.
49✔
266
         */
49✔
267
        async function getCategoryLoanCountByYear(categoryId, year, fetchPolicy = 'cache-first') {
49✔
NEW
268
                try {
×
NEW
269
                        const progress = await getCategoriesProgressByYear(year, fetchPolicy);
×
NEW
270
                        const count = progress?.find(entry => entry.id === categoryId)?.progressForYear || 0;
×
NEW
271
                        return count;
×
NEW
272
                } catch (error) {
×
NEW
273
                        logFormatter(error, 'Failed to fetch category loan count by year');
×
NEW
274
                        return null;
×
NEW
275
                }
×
NEW
276
        }
×
277

278
        async function loadPreferences(fetchPolicy = 'cache-first') {
49✔
279
                try {
33✔
280
                        const response = await apolloClient.query({ query: useGoalDataQuery, fetchPolicy });
33✔
281
                        const prefsData = response.data?.my?.userPreferences || null;
33✔
282
                        totalLoanCount.value = response.data?.my?.loans?.totalCount || 0;
33✔
283
                        userPreferences.value = prefsData;
33✔
284
                        return prefsData ? JSON.parse(prefsData.preferences || '{}') : {};
33!
285
                } catch (error) {
33✔
286
                        logFormatter(error, 'Failed to load preferences');
1✔
287
                        return null;
1✔
288
                }
1✔
289
        }
33✔
290

291
        async function loadProgress(year, fetchPolicy = 'network-only') {
49✔
292
                try {
27✔
293
                        const progress = await getCategoriesProgressByYear(year, fetchPolicy);
27✔
294
                        currentYearProgress.value = progress;
27✔
295
                } catch (error) {
27!
UNCOV
296
                        logFormatter(error, 'Failed to load progress');
×
UNCOV
297
                        return null;
×
UNCOV
298
                }
×
299
        }
27✔
300

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

361
        async function storeGoalPreferences(updates) {
49✔
362
                if (!userPreferences.value?.id) {
5✔
363
                        await createUserPreferences(apolloClient, { goals: [] });
2✔
364
                        await loadPreferences('network-only'); // Reload after create
2✔
365
                }
2✔
366
                const parsedPrefs = JSON.parse(userPreferences.value?.preferences || '{}');
5✔
367
                const goals = parsedPrefs.goals || [];
5✔
368
                const goalIndex = goals.findIndex(g => g.goalName === updates.goalName);
5✔
369
                if (goalIndex !== -1) {
5✔
370
                        goals[goalIndex] = { ...goals[goalIndex], ...updates };
3✔
371
                } else {
5✔
372
                        // When creating a new goal, set loanTotalAtStart to current all-time progress for the category
2✔
373
                        // This allows tracking progress from the point the goal was set
2✔
374
                        // For ID_SUPPORT_ALL, use totalLoanCount since it tracks total loans, not category-specific progress
2✔
375
                        let loanTotalAtStart;
2✔
376
                        if (updates.category === ID_SUPPORT_ALL) {
2!
377
                                loanTotalAtStart = totalLoanCount.value || 0;
×
378
                        } else {
2✔
379
                                const categoryProgress = currentYearProgress.value.find(n => n.id === updates.category);
2✔
380
                                loanTotalAtStart = categoryProgress?.totalProgressToAchievement || 0;
2✔
381
                        }
2✔
382
                        goals.push({ ...updates, loanTotalAtStart });
2✔
383
                }
2✔
384
                await updateUserPreferences(
5✔
385
                        apolloClient,
5✔
386
                        userPreferences.value,
5✔
387
                        parsedPrefs,
5✔
388
                        { goals }
5✔
389
                );
5✔
390
                setGoalState({ goals }); // Refresh local state after update
5✔
391
        }
5✔
392

393
        async function checkCompletedGoal({ currentGoalProgress = 0, category = 'post-checkout' } = {}) {
49✔
394
                // Skip if goal is already completed or expired
4✔
395
                if ([GOAL_STATUS.COMPLETED, GOAL_STATUS.EXPIRED].includes(userGoal.value?.status)) {
4✔
396
                        return;
1✔
397
                }
1✔
398
                if (
3✔
399
                        (currentGoalProgress && (currentGoalProgress >= userGoal.value?.target))
4✔
400
                        || (userGoal.value && userGoalAchieved.value)
4✔
401
                ) {
4✔
402
                        userGoal.value = {
2✔
403
                                ...userGoal.value,
2✔
404
                                status: GOAL_STATUS.COMPLETED
2✔
405
                        };
2✔
406
                        await storeGoalPreferences({ ...userGoal.value });
2✔
407
                        $kvTrackEvent(
2✔
408
                                category,
2✔
409
                                'show',
2✔
410
                                'annual-goal-complete',
2✔
411
                                userGoal.value.category,
2✔
412
                                userGoal.value.target
2✔
413
                        );
2✔
414
                        userGoalAchievedNow.value = true;
2✔
415
                }
2✔
416
        }
4✔
417

418
        /**
49✔
419
         * Check and correct negative goal progress
49✔
420
         * This could happen due to a race condition with postCheckoutAchievements
49✔
421
         * Only applies when yearlyProgress is false (all-time progress mode)
49✔
422
         */
49✔
423
        async function correctNegativeProgress() {
49✔
424
                if (useYearlyProgress.value || !userGoal.value || !currentYearProgress?.value?.length) return;
27✔
425

426
                const goal = userGoal.value;
11✔
427
                if (goal.category === ID_SUPPORT_ALL) return;
27✔
428

429
                const categoryProgress = currentYearProgress.value.find(n => n.id === goal.category);
8✔
430
                const allTimeProgress = categoryProgress?.totalProgressToAchievement || 0;
27✔
431
                const loanTotalAtStart = goal.loanTotalAtStart || 0;
27✔
432
                const adjustedProgress = allTimeProgress - loanTotalAtStart;
27✔
433

434
                if (adjustedProgress < 0) {
27!
435
                        const debugData = {
×
436
                                category: goal.category,
×
437
                                goalName: goal.goalName,
×
438
                                allTimeProgress,
×
439
                                loanTotalAtStart,
×
440
                                adjustedProgress,
×
441
                                target: goal.target,
×
442
                        };
×
443

444
                        logFormatter('Negative goal progress detected, correcting loanTotalAtStart', 'warn', debugData);
×
445

446
                        // Correct the goal by updating loanTotalAtStart to allTimeProgress
×
447
                        await storeGoalPreferences({
×
448
                                ...goal,
×
449
                                loanTotalAtStart: allTimeProgress,
×
450
                        });
×
451
                }
×
452
        }
27✔
453

454
        async function loadGoalData({
49✔
455
                loans = [], // Loans already in basket, used to initialize in-page counter for ID_SUPPORT_ALL
27✔
456
                year = new Date().getFullYear(),
27✔
457
                yearlyProgress = false, // thankyou_page_goals_enable flag - when true uses yearly, when false uses all-time
27✔
458
        } = {}) {
27✔
459
                loading.value = true;
27✔
460
                useYearlyProgress.value = yearlyProgress;
27✔
461
                const parsedPrefs = await loadPreferences();
27✔
462
                await loadProgress(year);
27✔
463
                setGoalState(parsedPrefs);
27✔
464
                // Initialize in-page counter for ID_SUPPORT_ALL based on loans already in basket
27✔
465
                if (userGoal.value?.category === ID_SUPPORT_ALL && loans.length > 0 && !goalCurrentLoanCount.value) {
27!
466
                        // Reducing counter by 1 because loans already has the added loan
×
467
                        goalCurrentLoanCount.value = loans.length - 1;
×
468
                }
×
469
                // Check and correct negative progress after loading
27✔
470
                await correctNegativeProgress();
27✔
471
                loading.value = false;
27✔
472
        }
27✔
473

474
        /**
49✔
475
         * This method renew goals annually.
49✔
476
         * It invalidates all goals on Jan 1st of 2026
49✔
477
         * @return {Array} - expiredGoals
49✔
478
         */
49✔
479
        async function renewAnnualGoal(today = new Date()) {
49✔
480
                const parsedPrefs = await loadPreferences();
3✔
481
                const goals = parsedPrefs.goals || [];
3!
482
                const currentYear = today.getFullYear();
3✔
483
                const renewedYear = parsedPrefs.goalsRenewedDate ? new Date(parsedPrefs.goalsRenewedDate).getFullYear() : null;
3✔
484
                const areGoalsRenewed = goals.some(goal => goal.status === GOAL_STATUS.EXPIRED);
3✔
485
                if (renewedYear > currentYear || areGoalsRenewed) {
3!
486
                        return {
×
487
                                expiredGoals: goals,
×
488
                                showRenewedAnnualGoalToast: false,
×
489
                        };
×
490
                }
×
491

492
                // Renew goals every following year
3✔
493
                const expiredGoals = goals.map(goal => {
3✔
494
                        const goalYear = goal.dateStarted ? new Date(goal.dateStarted).getFullYear() : null;
3!
495
                        if (goalYear < currentYear) {
3✔
496
                                return {
2✔
497
                                        ...goal,
2✔
498
                                        status: GOAL_STATUS.EXPIRED
2✔
499
                                };
2✔
500
                        }
2✔
501
                        return null;
1✔
502
                }).filter(goal => goal !== null);
3✔
503

504
                if (expiredGoals.some(goal => goal.status === GOAL_STATUS.EXPIRED)) {
3✔
505
                        parsedPrefs.goals = expiredGoals;
2✔
506
                        parsedPrefs.goalsRenewedDate = today.toISOString();
2✔
507
                        await updateUserPreferences(
2✔
508
                                apolloClient,
2✔
509
                                userPreferences.value,
2✔
510
                                parsedPrefs,
2✔
511
                                { goals: expiredGoals }
2✔
512
                        );
2✔
513
                        setGoalState({ goals: expiredGoals });
2✔
514
                }
2✔
515

516
                const showRenewedAnnualGoalToast = !!expiredGoals.length
3✔
517
                        && !expiredGoals.some(g => g.status === GOAL_STATUS.COMPLETED);
3✔
518

519
                return {
3✔
520
                        expiredGoals,
3✔
521
                        showRenewedAnnualGoalToast,
3✔
522
                };
3✔
523
        }
3✔
524

525
        async function setHideGoalCardPreference() {
49✔
526
                const parsedPrefs = await loadPreferences();
1✔
527
                await updateUserPreferences(
1✔
528
                        apolloClient,
1✔
529
                        userPreferences.value,
1✔
530
                        parsedPrefs,
1✔
531
                        { hideGoalCard: true }
1✔
532
                );
1✔
533
        }
1✔
534

535
        function hideGoalCard() {
49✔
536
                const parsedPrefs = JSON.parse(userPreferences.value?.preferences || '{}');
2✔
537
                return parsedPrefs.hideGoalCard || false;
2✔
538
        }
2✔
539

540
        return {
49✔
541
                checkCompletedGoal,
49✔
542
                getCategories,
49✔
543
                getCategoriesProgressByYear,
49✔
544
                getCategoryLoanCountByYear,
49✔
545
                getCategoryLoansLastYear,
49✔
546
                getCtaHref,
49✔
547
                getGoalDisplayName,
49✔
548
                getPostCheckoutProgressByLoans,
49✔
549
                isProgressCompletingGoal,
49✔
550
                loadGoalData,
49✔
551
                storeGoalPreferences,
49✔
552
                goalProgress,
49✔
553
                loading,
49✔
554
                userGoal,
49✔
555
                userGoalAchieved,
49✔
556
                userGoalAchievedNow,
49✔
557
                userPreferences,
49✔
558
                // Goal Entry for 2026 Goals
49✔
559
                renewAnnualGoal,
49✔
560
                hideGoalCard,
49✔
561
                setHideGoalCardPreference,
49✔
562
        };
49✔
563
}
49✔
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