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

kiva / ui / 19438547198

17 Nov 2025 05:26PM UTC coverage: 91.406% (-0.05%) from 91.457%
19438547198

Pull #6452

github

web-flow
Merge 1ed0f1eeb into 744df631c
Pull Request #6452: feat: transition 2025 to 2026 goals, and adding flags to show goal entry

3736 of 3999 branches covered (93.42%)

Branch coverage included in aggregate %.

43 of 58 new or added lines in 1 file covered. (74.14%)

9 existing lines in 1 file now uncovered.

19026 of 20903 relevant lines covered (91.02%)

78.22 hits per line

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

89.68
/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 logFormatter from '#src/util/logFormatter';
1✔
10
import { createUserPreferences, updateUserPreferences } from '#src/util/userPreferenceUtils';
1✔
11

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

21
const GOAL_DISPLAY_MAP = {
1✔
22
        [ID_BASIC_NEEDS]: 'basic needs loans',
1✔
23
        [ID_CLIMATE_ACTION]: 'eco-friendly loans',
1✔
24
        [ID_REFUGEE_EQUALITY]: 'refugees',
1✔
25
        [ID_SUPPORT_ALL]: 'loans',
1✔
26
        [ID_US_ECONOMIC_EQUALITY]: 'U.S. entrepreneurs',
1✔
27
        [ID_WOMENS_EQUALITY]: 'women',
1✔
28
};
1✔
29

30
const GOAL_1_DISPLAY_MAP = {
1✔
31
        [ID_BASIC_NEEDS]: 'basic needs loan',
1✔
32
        [ID_CLIMATE_ACTION]: 'eco-friendly loan',
1✔
33
        [ID_REFUGEE_EQUALITY]: 'refugee',
1✔
34
        [ID_SUPPORT_ALL]: 'loan',
1✔
35
        [ID_US_ECONOMIC_EQUALITY]: 'U.S. entrepreneur',
1✔
36
        [ID_WOMENS_EQUALITY]: 'woman',
1✔
37
};
1✔
38

39
function getGoalDisplayName(target, category) {
16✔
40
        if (!target || target > 1) return GOAL_DISPLAY_MAP[category] || 'loans';
16✔
41
        return GOAL_1_DISPLAY_MAP[category] || 'loan';
16✔
42
}
16✔
43

44
/**
1✔
45
 * Vue composable for loading and managing user goal data
1✔
46
 *
1✔
47
 * @param {Object} options - Configuration options
1✔
48
 * @param {Array} options.loans - List of loans to count toward goals
1✔
49
 * @param {Object} options.apollo - Apollo client instance (optional, will use inject if not provided)
1✔
50
 * @returns Goal data and utilities
1✔
51
 */
1✔
52
export default function useGoalData({ apollo }) {
1✔
53
        const $kvTrackEvent = inject('$kvTrackEvent');
28✔
54

55
        const allTimeProgress = ref([]);
28✔
56
        const loading = ref(true);
28✔
57
        const totalLoanCount = ref(null);
28✔
58
        const userGoal = ref(null);
28✔
59
        const userPreferences = ref(null);
28✔
60
        const userGoalAchievedNow = ref(false);
28✔
61
        const goalCurrentLoanCount = ref(0); // Tracks loans toward "Support All" goal
28✔
62

63
        async function loadPreferences(fetchPolicy = 'cache-first') {
28✔
64
                try {
23✔
65
                        const response = await apollo.query({ query: useGoalDataQuery, fetchPolicy });
23✔
66
                        const prefsData = response.data?.my?.userPreferences || null;
23✔
67
                        totalLoanCount.value = response.data?.my?.loans?.totalCount || 0;
23✔
68
                        userPreferences.value = prefsData;
23✔
69
                        return prefsData ? JSON.parse(prefsData.preferences || '{}') : {};
23!
70
                } catch (error) {
23✔
71
                        logFormatter(error, 'Failed to load preferences');
1✔
72
                        return null;
1✔
73
                }
1✔
74
        }
23✔
75

76
        async function loadProgress(loans) {
28✔
77
                try {
21✔
78
                        const loanIds = loans.map(loan => loan.id);
21✔
79
                        const response = await apollo.query({
21✔
80
                                query: useGoalDataProgressQuery,
21✔
81
                                variables: { loanIds },
21✔
82
                        });
21✔
83
                        return response?.data?.postCheckoutAchievements?.allTimeProgress || [];
21✔
84
                } catch (error) {
21✔
85
                        logFormatter(error, 'Failed to load progress');
1✔
86
                        return null;
1✔
87
                }
1✔
88
        }
21✔
89

90
        function setGoalState(parsedPrefs) {
28✔
91
                if (!parsedPrefs) return;
29✔
92
                const goals = parsedPrefs.goals || [];
29✔
93
                userGoal.value = { ...goals[0] };
29✔
94
        }
29✔
95

96
        async function storeGoalPreferences(updates) {
28✔
97
                if (!userPreferences.value?.id) {
7✔
98
                        await createUserPreferences(apollo, { goals: [] });
3✔
99
                        await loadPreferences('network-only'); // Reload after create
3✔
100
                }
3✔
101
                const parsedPrefs = JSON.parse(userPreferences.value?.preferences || '{}');
7!
102
                const goals = parsedPrefs.goals || [];
7!
103
                const goalIndex = goals.findIndex(g => g.goalName === updates.goalName);
7✔
104
                if (goalIndex !== -1) goals[goalIndex] = { ...goals[goalIndex], ...updates };
7✔
105
                else goals.push(updates);
4✔
106
                await updateUserPreferences(apollo, userPreferences.value, parsedPrefs, { goals });
7✔
107
                setGoalState({ goals }); // Refresh local state after update
7✔
108
        }
7✔
109

110
        const goalProgress = computed(() => {
28✔
111
                if (userGoal.value?.category === ID_SUPPORT_ALL) {
10✔
112
                        const currentTotal = totalLoanCount.value || 0;
3!
113
                        const startTotal = userGoal.value?.loanTotalAtStart || 0;
3!
114
                        return Math.max(currentTotal - startTotal, 0);
3✔
115
                }
3✔
116
                const totalProgress = allTimeProgress.value.find(
7✔
117
                        entry => entry.achievementId === userGoal.value?.category
7✔
118
                )?.totalProgress || 0;
10✔
119
                const adjustedProgress = totalProgress - (userGoal.value?.loanTotalAtStart || 0);
10✔
120
                return Math.max(adjustedProgress, 0);
10✔
121
        });
28✔
122

123
        const userGoalAchieved = computed(() => goalProgress.value >= userGoal.value?.target);
28✔
124

125
        const checkCompletedGoal = async (category = 'post-checkout') => {
28✔
126
                if (userGoal.value && userGoalAchieved.value && userGoal.value.status !== 'completed') {
4✔
127
                        await storeGoalPreferences({
2✔
128
                                goalName: userGoal.value.goalName,
2✔
129
                                dateStarted: userGoal.value.dateStarted,
2✔
130
                                target: userGoal.value.target,
2✔
131
                                count: userGoal.value.count,
2✔
132
                                status: 'completed',
2✔
133
                        });
2✔
134
                        $kvTrackEvent(
2✔
135
                                category,
2✔
136
                                'show',
2✔
137
                                'annual-goal-complete',
2✔
138
                                userGoal.value.category,
2✔
139
                                userGoal.value.target
2✔
140
                        );
2✔
141
                        userGoalAchievedNow.value = true;
2✔
142
                }
2✔
143
        };
28✔
144

145
        const getProgressByLoan = async loan => {
28✔
146
                const result = await loadProgress([loan]);
2✔
147
                if (userGoal.value?.category === ID_SUPPORT_ALL) {
2!
UNCOV
148
                        goalCurrentLoanCount.value += 1;
×
UNCOV
149
                        return goalCurrentLoanCount.value;
×
UNCOV
150
                }
×
151

152
                const totalProgress = result.find(
2✔
153
                        entry => entry.achievementId === userGoal.value?.category
2✔
154
                )?.totalProgress || 0;
2✔
155
                return totalProgress;
2✔
156
        };
28✔
157

158
        const setCurrentLoanCount = loansCount => {
28✔
159
                const currentTotal = totalLoanCount.value || 0;
3!
160
                const startTotal = userGoal.value?.loanTotalAtStart || 0;
3!
161

162
                goalCurrentLoanCount.value = Math.max(currentTotal - startTotal, 0) + loansCount;
3✔
163
        };
28✔
164

165
        async function loadGoalData(loans = []) {
28✔
166
                loading.value = true;
19✔
167
                const parsedPrefs = await loadPreferences();
19✔
168
                allTimeProgress.value = await loadProgress(loans);
19✔
169
                setGoalState(parsedPrefs);
19✔
170
                if (userGoal.value?.category === ID_SUPPORT_ALL && !goalCurrentLoanCount.value) {
19✔
171
                        // Reducing counter by 1 because loans already has the added loan
3✔
172
                        setCurrentLoanCount(loans.length - 1);
3✔
173
                }
3✔
174

175
                loading.value = false;
19✔
176
        }
19✔
177

178
        /**
28✔
179
         * Replaces all goals in user preferences with newGoals
28✔
180
         * @param {*} newGoals - Array of new goal objects to set
28✔
181
         */
28✔
182
        async function replaceAllGoals(newGoals) {
28✔
183
                if (!userPreferences.value?.id) {
3!
NEW
184
                        await createUserPreferences(apollo, { goals: [] });
×
NEW
185
                        await loadPreferences('network-only');
×
NEW
186
                }
×
187
                const parsedPrefs = JSON.parse(userPreferences.value?.preferences || '{}');
3!
188
                parsedPrefs.goals = newGoals;
3✔
189
                await updateUserPreferences(apollo, userPreferences.value, parsedPrefs, { goals: newGoals });
3✔
190
                setGoalState({ goals: newGoals });
3✔
191
        }
3✔
192

193
        /**
28✔
194
         * This method shows Goal Entry for 2026 Goals
28✔
195
         * It invalidates all goals on Jan 1st of 2026
28✔
196
         * @return {Array} - expiredGoals
28✔
197
         */
28✔
198
        async function renewAnnualGoal(today = new Date()) {
28✔
199
                const parsedPrefs = await loadPreferences();
1✔
200
                const goals = parsedPrefs.goals || [];
1!
201
                let expiredGoals = [];
1✔
202

203
                if (today.getMonth() === 0 && today.getDate() === 1) {
1✔
204
                        expiredGoals = goals.map(prevGoal => ({
1✔
NEW
205
                                ...prevGoal,
×
NEW
206
                                active: false
×
207
                        }));
1✔
208
                        await replaceAllGoals(expiredGoals);
1✔
209
                }
1✔
210

211
                return expiredGoals;
1✔
212
        }
1✔
213

214
        /**
28✔
215
         * Determines if the renew goal toast should be shown
28✔
216
         */
28✔
217
        const showRenewedAnnualGoalToast = computed(() => {
28✔
NEW
218
                const parsedPrefs = userPreferences.value
×
NEW
219
                        ? JSON.parse(userPreferences.value.preferences || '{}')
×
NEW
UNCOV
220
                        : {};
×
NEW
UNCOV
221
                const goals = parsedPrefs.goals || [];
×
NEW
UNCOV
222
                return !goals.some(goal => !goal?.active && goal.status === 'completed');
×
223
        });
28✔
224

225
        /**
28✔
226
         * Determines if all goals are renewed (inactive)
28✔
227
         */
28✔
228
        const goalsAreRenewed = computed(() => {
28✔
NEW
UNCOV
229
                const parsedPrefs = userPreferences.value
×
NEW
UNCOV
230
                        ? JSON.parse(userPreferences.value.preferences || '{}')
×
NEW
231
                        : {};
×
NEW
232
                const goals = parsedPrefs.goals || [];
×
233

NEW
UNCOV
234
                return goals.every(goal => !goal?.active);
×
235
        });
28✔
236

237
        return {
28✔
238
                getGoalDisplayName,
28✔
239
                goalProgress,
28✔
240
                loading,
28✔
241
                loadGoalData,
28✔
242
                storeGoalPreferences,
28✔
243
                userGoal,
28✔
244
                userGoalAchieved,
28✔
245
                userGoalAchievedNow,
28✔
246
                checkCompletedGoal,
28✔
247
                getProgressByLoan,
28✔
248
                // Goal Entry for 2026 Goals
28✔
249
                userPreferences,
28✔
250
                replaceAllGoals,
28✔
251
                renewAnnualGoal,
28✔
252
                showRenewedAnnualGoalToast,
28✔
253
                goalsAreRenewed,
28✔
254
        };
28✔
255
}
28✔
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