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

steinarb / ukelonn / #249

13 Feb 2025 09:17PM UTC coverage: 98.612% (-1.1%) from 99.721%
#249

push

steinarb
Merge branch 'work/using-react-with-bootstrap4'

5 of 5 new or added lines in 1 file covered. (100.0%)

12 existing lines in 1 file now uncovered.

1066 of 1081 relevant lines covered (98.61%)

0.99 hits per line

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

97.23
/ukelonn.backend/src/main/java/no/priv/bang/ukelonn/backend/UkelonnServiceProvider.java
1
/*
2
 * Copyright 2016-2024 Steinar Bang
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *   http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing,
11
 * software distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and limitations
14
 * under the License.
15
 */
16
package no.priv.bang.ukelonn.backend;
17

18
import org.osgi.service.component.annotations.Activate;
19
import org.osgi.service.component.annotations.Component;
20
import org.osgi.service.component.annotations.Reference;
21
import org.osgi.service.log.LogService;
22
import org.osgi.service.log.Logger;
23

24
import java.io.ByteArrayOutputStream;
25
import java.nio.charset.StandardCharsets;
26
import java.sql.PreparedStatement;
27
import java.sql.ResultSet;
28
import java.sql.SQLException;
29
import java.sql.Timestamp;
30
import java.util.ArrayList;
31
import java.util.Arrays;
32
import java.util.Collections;
33
import java.util.Date;
34
import java.util.HashMap;
35
import java.util.List;
36
import java.util.Locale;
37
import java.util.Map;
38
import java.util.Optional;
39
import java.util.ResourceBundle;
40
import java.util.concurrent.ConcurrentHashMap;
41
import java.util.concurrent.ConcurrentLinkedQueue;
42
import javax.sql.DataSource;
43

44
import no.priv.bang.authservice.definitions.AuthserviceException;
45
import no.priv.bang.osgiservice.users.Role;
46
import no.priv.bang.osgiservice.users.UserManagementService;
47
import no.priv.bang.osgiservice.users.UserRoles;
48
import no.priv.bang.ukelonn.UkelonnException;
49
import no.priv.bang.ukelonn.UkelonnService;
50
import no.priv.bang.ukelonn.beans.Account;
51
import no.priv.bang.ukelonn.beans.Bonus;
52
import no.priv.bang.ukelonn.beans.LocaleBean;
53
import no.priv.bang.ukelonn.beans.Notification;
54
import no.priv.bang.ukelonn.beans.PasswordsWithUser;
55
import no.priv.bang.ukelonn.beans.PerformedTransaction;
56
import no.priv.bang.ukelonn.beans.SumYear;
57
import no.priv.bang.ukelonn.beans.SumYearMonth;
58
import no.priv.bang.ukelonn.beans.Transaction;
59
import no.priv.bang.ukelonn.beans.TransactionType;
60
import no.priv.bang.ukelonn.beans.UpdatedTransaction;
61
import no.priv.bang.ukelonn.beans.User;
62
import static no.priv.bang.ukelonn.UkelonnConstants.*;
63

64
/**
65
 * The OSGi component that provides the business logic of the ukelonn
66
 * webapp.
67
 *
68
 * @author Steinar Bang
69
 *
70
 */
71
@Component(service=UkelonnService.class, immediate=true, property= { "defaultlocale=nb_NO" })
72
public class UkelonnServiceProvider extends UkelonnServiceBase {
1✔
73
    private static final String RESOURCES_BASENAME = "i18n.ApplicationResources";
74
    private DataSource datasource;
75
    private UserManagementService useradmin;
76
    private LogService logservice;
77
    private Logger logger;
78
    private ConcurrentHashMap<String, ConcurrentLinkedQueue<Notification>> notificationQueues = new ConcurrentHashMap<>();
1✔
79
    private Locale defaultLocale;
80
    static final String LAST_NAME = "last_name";
81
    static final String FIRST_NAME = "first_name";
82
    static final String USERNAME = "username";
83
    static final int NUMBER_OF_TRANSACTIONS_TO_DISPLAY = 10;
84
    static final String USER_ID = "user_id";
85

86
    @Activate
87
    public void activate(Map<String, Object> config) {
88
        defaultLocale = Locale.forLanguageTag(((String) config.get("defaultlocale")).replace('_', '-'));
1✔
89
        addRolesIfNotPresent();
1✔
90
    }
1✔
91

92
    @Reference(target = "(osgi.jndi.service.name=jdbc/ukelonn)")
93
    public void setDataSource(DataSource datasource) {
94
        this.datasource = datasource;
1✔
95
    }
1✔
96

97
    @Override
98
    public DataSource getDataSource() {
99
        return datasource;
1✔
100
    }
101

102
    @Reference
103
    public void setUserAdmin(UserManagementService useradmin) {
104
        this.useradmin = useradmin;
1✔
105
    }
1✔
106

107
    @Reference
108
    public void setLogservice(LogService logservice) {
109
        this.logservice = logservice;
1✔
110
        this.logger = logservice.getLogger(getClass());
1✔
111
    }
1✔
112

113
    @Override
114
    public LogService getLogservice() {
115
        return logservice;
1✔
116
    }
117

118
    @Override
119
    public List<Account> getAccounts() {
120
        var accounts = new ArrayList<Account>();
1✔
121
        try(var connection = datasource.getConnection()) {
1✔
122
            try(var statement = connection.prepareStatement("select account_id, username, balance from accounts_view")) {
1✔
123
                try(var results = statement.executeQuery()) {
1✔
124
                    if (results != null) {
1✔
125
                        while(results.next()) {
1✔
126
                            var newaccount = mapAccount(results);
1✔
127
                            accounts.add(newaccount);
1✔
128
                        }
1✔
129
                    }
130
                }
131
            }
132
        } catch (SQLException e) {
1✔
133
            // Log and continue
134
            logError("Error when getting all accounts from the database", e);
1✔
135
        }
1✔
136

137
        return accounts;
1✔
138
    }
139

140
    @Override
141
    public Account getAccount(String username) {
142
        try(var connection = datasource.getConnection()) {
1✔
143
            try(var statement = connection.prepareStatement("select account_id, username, balance from accounts_view where username=?")) {
1✔
144
                statement.setString(1, username);
1✔
145
                try(var resultset = statement.executeQuery()) {
1✔
146
                    if (resultset.next())
1✔
147
                    {
148
                        return mapAccount(resultset);
1✔
149
                    }
150

151
                    throw new UkelonnException(String.format("Got an empty ResultSet while fetching account from the database for user \\\"%s\\\"", username));
1✔
152
                }
153

154
            }
155
        } catch (SQLException e) {
1✔
156
            throw new UkelonnException(String.format("Caught SQLException while fetching account from the database for user \"%s\"", username), e);
1✔
157
        }
158
    }
159

160
    @Override
161
    public Account registerPerformedJob(PerformedTransaction job) {
162
        var accountId = job.account().accountId();
1✔
163
        var jobtypeId = job.transactionTypeId();
1✔
164
        var jobamount = addBonus(job.transactionAmount());
1✔
165
        var timeofjob = job.transactionDate();
1✔
166
        try(var connection = datasource.getConnection()) {
1✔
167
            try(var statement = connection.prepareStatement("insert into transactions (account_id, transaction_type_id,transaction_amount, transaction_time) values (?, ?, ?, ?)")) {
1✔
168
                statement.setInt(1, accountId);
1✔
169
                statement.setInt(2, jobtypeId);
1✔
170
                statement.setDouble(3, jobamount);
1✔
171
                statement.setTimestamp(4, new java.sql.Timestamp(timeofjob.getTime()));
1✔
172
                statement.executeUpdate();
1✔
173
            }
174
        } catch (SQLException exception) {
1✔
175
            var message = String.format("Failed to register performed job in the database, account: %d  jobtype: %d  amount: %f", accountId, jobtypeId, jobamount);
1✔
176
            logError(message, exception);
1✔
177
        }
1✔
178

179
        return getAccount(job.account().username());
1✔
180
    }
181

182
    @Override
183
    public List<TransactionType> getJobTypes() {
184
        var jobtypes = new ArrayList<TransactionType>();
1✔
185
        try(var connection = datasource.getConnection()) {
1✔
186
            try(var statement = connection.prepareStatement("select transaction_type_id, transaction_type_name, transaction_amount, transaction_is_work, transaction_is_wage_payment from transaction_types where transaction_is_work=true")) {
1✔
187
                try(var resultSet = statement.executeQuery()) {
1✔
188
                    if (resultSet != null) {
1✔
189
                        while (resultSet.next()) {
1✔
190
                            var transactiontype = UkelonnServiceProvider.mapTransactionType(resultSet);
1✔
191
                            jobtypes.add(transactiontype);
1✔
192
                        }
1✔
193
                    }
194
                }
195
            }
196
        } catch (SQLException e) {
1✔
197
            logError("Error getting job types from the database", e);
1✔
198
        }
1✔
199

200
        return jobtypes;
1✔
201
    }
202

203
    @Override
204
    public List<Transaction> getJobs(int accountId) {
205
        return getTransactionsFromAccount(accountId, "/sql/query/jobs_last_n.sql", "job");
1✔
206
    }
207

208
    @Override
209
    public List<Transaction> getPayments(int accountId) {
210
        var payments = getTransactionsFromAccount(accountId, "/sql/query/payments_last_n.sql", "payments");
1✔
211
        payments = UkelonnServiceProvider.makePaymentAmountsPositive(payments); // Payments are negative numbers in the DB, presented as positive numbers in the GUI
1✔
212
        return payments;
1✔
213
    }
214

215
    List<Transaction> getTransactionsFromAccount(int accountId,
216
                                                 String sqlTemplate,
217
                                                 String transactionType)
218
    {
219
        var transactions = new ArrayList<Transaction>();
1✔
220
        var sql = String.format(getResourceAsString(sqlTemplate), UkelonnServiceProvider.NUMBER_OF_TRANSACTIONS_TO_DISPLAY);
1✔
221
        try(var connection = datasource.getConnection()) {
1✔
222
            try(var statement = connection.prepareStatement(sql)) {
1✔
223
                statement.setInt(1, accountId);
1✔
224
                trySettingPreparedStatementParameterThatMayNotBePresent(statement, 2, accountId);
1✔
225
                try(var resultSet = statement.executeQuery()) {
1✔
226
                    while (resultSet.next()) {
1✔
227
                        transactions.add(UkelonnServiceProvider.mapTransaction(resultSet));
1✔
228
                    }
229
                }
230
            }
231
        } catch (SQLException e) {
1✔
232
            logError("Error getting "+transactionType+"s from the database", e);
1✔
233
        }
1✔
234

235
        return transactions;
1✔
236
    }
237

238
    @Override
239
    public List<Transaction> deleteJobsFromAccount(int accountId, List<Integer> idsOfJobsToDelete) {
240
        if (!idsOfJobsToDelete.isEmpty()) {
1✔
241
            var deleteQuery = "delete from transactions where transaction_id in (select transaction_id from transactions inner join transaction_types on transactions.transaction_type_id=transaction_types.transaction_type_id where transaction_id in (" + joinIds(idsOfJobsToDelete) + ") and transaction_types.transaction_is_work=? and account_id=?)";
1✔
242
            try(var connection = datasource.getConnection()) {
1✔
243
                try (var statement = connection.prepareStatement(deleteQuery)) { // NOSONAR This string manipulation is OK and the only way to do it
1✔
244
                    addParametersToDeleteJobsStatement(accountId, statement);
1✔
245
                    statement.executeUpdate();
1✔
246
                }
247
            } catch (SQLException e) {
1✔
248
                String message = String.format("Failed to delete jobs from accountId: %d", accountId);
1✔
249
                logError(message, e);
1✔
250
            }
1✔
251
        }
252

253
        return getJobs(accountId);
1✔
254
    }
255

256
    void addParametersToDeleteJobsStatement(int accountId, PreparedStatement statement) {
257
        try {
258
            statement.setBoolean(1, true);
1✔
259
            statement.setInt(2, accountId);
1✔
260
        } catch (SQLException e) {
1✔
261
            String message = "Caught exception adding parameters to job delete statement";
1✔
262
            logger.error(message, e);
1✔
263
            throw new UkelonnException(message, e);
1✔
264
        }
1✔
265
    }
1✔
266

267
    @Override
268
    public List<Transaction> updateJob(UpdatedTransaction editedJob) {
269
        var sql = "update transactions set transaction_type_id=?, transaction_time=?, transaction_amount=? where transaction_id=?";
1✔
270
        try(var connection = datasource.getConnection()) {
1✔
271
            try(var statement = connection.prepareStatement(sql)) {
1✔
272
                statement.setInt(1, editedJob.transactionTypeId());
1✔
273
                statement.setTimestamp(2, new java.sql.Timestamp(editedJob.transactionTime().getTime()));
1✔
274
                statement.setDouble(3, editedJob.transactionAmount());
1✔
275
                statement.setInt(4, editedJob.id());
1✔
276
                statement.executeUpdate();
1✔
277
            }
278
        } catch (SQLException e) {
1✔
279
            throw new UkelonnException(String.format("Failed to update job with id %d", editedJob.id()) , e);
1✔
280
        }
1✔
281

282
        return getJobs(editedJob.accountId());
1✔
283
    }
284

285
    @Override
286
    public List<TransactionType> getPaymenttypes() {
287
        var paymenttypes = new ArrayList<TransactionType>();
1✔
288
        try(var connection = datasource.getConnection()) {
1✔
289
            try(var statement = connection.prepareStatement("select transaction_type_id, transaction_type_name, transaction_amount, transaction_is_work, transaction_is_wage_payment from transaction_types where transaction_is_wage_payment=true")) {
1✔
290
                try(var resultSet = statement.executeQuery()) {
1✔
291
                    if (resultSet != null) {
1✔
292
                        while (resultSet.next()) {
1✔
293
                            var transactiontype = UkelonnServiceProvider.mapTransactionType(resultSet);
1✔
294
                            paymenttypes.add(transactiontype);
1✔
295
                        }
1✔
296
                    }
297
                }
298
            }
299
        } catch (SQLException e) {
1✔
300
            logError("Error getting payment types from the database", e);
1✔
301
        }
1✔
302

303
        return paymenttypes;
1✔
304
    }
305

306
    @Override
307
    public Account registerPayment(PerformedTransaction payment) {
308
        var accountId = payment.account().accountId();
1✔
309
        var transactionTypeId = payment.transactionTypeId();
1✔
310
        var amount = 0 - payment.transactionAmount();
1✔
311
        var transactionDate = new Date();
1✔
312
        try(var connection = datasource.getConnection()) {
1✔
313
            try(var statement = connection.prepareStatement("insert into transactions (account_id,transaction_type_id,transaction_amount, transaction_time) values (?, ?, ?, ?)")) {
1✔
314
                statement.setInt(1, accountId);
1✔
315
                statement.setInt(2, transactionTypeId);
1✔
316
                statement.setDouble(3, amount);
1✔
317
                statement.setTimestamp(4, new java.sql.Timestamp(transactionDate.getTime()));
1✔
318
                statement.executeUpdate();
1✔
319
            }
320
        } catch (SQLException e) {
1✔
321
            var message = String.format("Failed to register payment  accountId: %d  transactionTypeId: %d  amount: %f", accountId, transactionTypeId, amount);
1✔
322
            logError(message, e);
1✔
323
            return null;
1✔
324
        }
1✔
325

326
        return getAccount(payment.account().username());
1✔
327
    }
328

329
    @Override
330
    public List<TransactionType> modifyJobtype(TransactionType jobtype) {
331
        try(var connection = datasource.getConnection()) {
1✔
332
            try(var statement = connection.prepareStatement("update transaction_types set transaction_type_name=?, transaction_amount=?, transaction_is_work=true, transaction_is_wage_payment=false where transaction_type_id=?")) {
1✔
333
                statement.setString(1, jobtype.transactionTypeName());
1✔
334
                statement.setDouble(2, jobtype.transactionAmount());
1✔
335
                statement.setInt(3, jobtype.id());
1✔
336
                statement.executeUpdate();
1✔
337
            }
338
        } catch (SQLException e) {
1✔
339
            var message = String.format("Failed to update jobtype %d in the database", jobtype.id());
1✔
340
            logError(message, e);
1✔
341
            throw new UkelonnException(message, e);
1✔
342
        }
1✔
343

344
        return getJobTypes();
1✔
345
    }
346

347
    @Override
348
    public List<TransactionType> createJobtype(TransactionType jobtype) {
349
        try(var connection = datasource.getConnection()) {
1✔
350
            try(var statement = connection.prepareStatement("insert into transaction_types (transaction_type_name, transaction_amount, transaction_is_work, transaction_is_wage_payment) values (?, ?, true, false)")) {
1✔
351
                statement.setString(1, jobtype.transactionTypeName());
1✔
352
                statement.setObject(2, jobtype.transactionAmount());
1✔
353
                statement.executeUpdate();
1✔
354
            }
355
        } catch (SQLException e) {
1✔
356
            var message = String.format("Failed to create jobtype \"%s\" in the database", jobtype.transactionTypeName());
1✔
357
            logError(message, e);
1✔
358
            throw new UkelonnException(message, e);
1✔
359
        }
1✔
360

361
        return getJobTypes();
1✔
362
    }
363

364
    @Override
365
    public List<TransactionType> modifyPaymenttype(TransactionType paymenttype) {
366
        try(var connection = datasource.getConnection()) {
1✔
367
            try(var statement = connection.prepareStatement("update transaction_types set transaction_type_name=?, transaction_amount=?, transaction_is_work=false, transaction_is_wage_payment=true where transaction_type_id=?")) {
1✔
368
                statement.setString(1, paymenttype.transactionTypeName());
1✔
369
                statement.setDouble(2, paymenttype.transactionAmount());
1✔
370
                statement.setInt(3, paymenttype.id());
1✔
371
                statement.executeUpdate();
1✔
372
            }
373
        } catch (SQLException e) {
1✔
374
            var message = String.format("Failed to update payment type %d in the database", paymenttype.id());
1✔
375
            logError(message, e);
1✔
376
            throw new UkelonnException(message, e);
1✔
377
        }
1✔
378

379
        return getPaymenttypes();
1✔
380
    }
381

382
    @Override
383
    public List<TransactionType> createPaymenttype(TransactionType paymenttype) {
384
        try(var connection = datasource.getConnection()) {
1✔
385
            try(var statement = connection.prepareStatement("insert into transaction_types (transaction_type_name, transaction_amount, transaction_is_work, transaction_is_wage_payment) values (?, ?, false, true)")) {
1✔
386
                statement.setString(1, paymenttype.transactionTypeName());
1✔
387
                statement.setObject(2, paymenttype.transactionAmount());
1✔
388
                statement.executeUpdate();
1✔
389
            }
390
        } catch (SQLException e) {
1✔
391
            var message = String.format("Failed to create payment type \"%s\" in the database", paymenttype.transactionTypeName());
1✔
392
            logError(message, e);
1✔
393
            throw new UkelonnException(message, e);
1✔
394
        }
1✔
395

396
        return getPaymenttypes();
1✔
397
    }
398

399
    @Override
400
    public Account addAccount(User user) {
UNCOV
401
        var username = user.username();
×
UNCOV
402
        try(var connection = datasource.getConnection()) {
×
UNCOV
403
            try(var insertAccountSql = connection.prepareStatement("insert into accounts (username) values (?)")) {
×
UNCOV
404
                insertAccountSql.setString(1, username);
×
UNCOV
405
                insertAccountSql.executeUpdate();
×
406
            }
407

UNCOV
408
            addDummyPaymentToAccountSoThatAccountWillAppearInAccountsView(username);
×
409

UNCOV
410
            return getAccount(user.username());
×
UNCOV
411
        } catch (SQLException e) {
×
UNCOV
412
            var message = "Database exception when account for new user";
×
UNCOV
413
            logger.error(message, e);
×
UNCOV
414
            throw new UkelonnException(message, e);
×
415
        }
416
    }
417

418
    @Override
419
    public List<SumYear> earningsSumOverYear(String username) {
420
        var statistics = new ArrayList<SumYear>();
1✔
421
        try(var connection = datasource.getConnection()) {
1✔
422
            try(var statement = connection.prepareStatement("select aggregate_amount, aggregate_year from sum_over_year_view where username=?")) {
1✔
423
                statement.setString(1, username);
1✔
424
                try(var resultSet = statement.executeQuery()) {
1✔
425
                    while (resultSet.next()) {
1✔
426
                        var sumYear = SumYear.with()
1✔
427
                            .sum(resultSet.getDouble(1))
1✔
428
                            .year(resultSet.getInt(2))
1✔
429
                            .build();
1✔
430
                        statistics.add(sumYear);
1✔
431
                    }
1✔
432
                }
433
            }
434
        } catch (SQLException e) {
1✔
435
            logWarning(String.format("Failed to get sum of earnings per year for account \"%s\" from the database", username), e);
1✔
436
        }
1✔
437

438
        return statistics;
1✔
439
    }
440

441
    @Override
442
    public List<SumYearMonth> earningsSumOverMonth(String username) {
443
        var statistics = new ArrayList<SumYearMonth>();
1✔
444
        try(var connection = datasource.getConnection()) {
1✔
445
            try(var statement = connection.prepareStatement("select aggregate_amount, aggregate_year, aggregate_month from sum_over_year_and_month_view where username=?")) {
1✔
446
                statement.setString(1, username);
1✔
447
                try(var resultSet = statement.executeQuery()) {
1✔
448
                    while (resultSet.next()) {
1✔
449
                        var sumYearMonth = SumYearMonth.with()
1✔
450
                            .sum(resultSet.getDouble(1))
1✔
451
                            .year(resultSet.getInt(2))
1✔
452
                            .month(resultSet.getInt(3))
1✔
453
                            .build();
1✔
454
                        statistics.add(sumYearMonth);
1✔
455
                    }
1✔
456
                }
457
            }
458
        } catch (SQLException e) {
1✔
459
            logWarning(String.format("Failed to get sum of earnings per month for account \"%s\" from the database", username), e);
1✔
460
        }
1✔
461

462
        return statistics;
1✔
463
    }
464

465
    @Override
466
    public List<Notification> notificationsTo(String username) {
467
        var notifications = getNotificationQueueForUser(username);
1✔
468
        var notification = notifications.poll();
1✔
469
        if (notification == null) {
1✔
470
            return Collections.emptyList();
1✔
471
        }
472

473
        return Arrays.asList(notification);
1✔
474
    }
475

476
    @Override
477
    public void notificationTo(String username, Notification notification) {
478
        var notifications = getNotificationQueueForUser(username);
1✔
479
        notifications.add(notification);
1✔
480
    }
1✔
481

482
    @Override
483
    public List<Bonus> getActiveBonuses() {
484
        var activebonuses = new ArrayList<Bonus>();
1✔
485
        try(var connection = datasource.getConnection()) {
1✔
486
            try(var statement = connection.prepareStatement("select bonus_id, enabled, iconurl, title, description, bonus_factor, start_date, end_date from bonuses where enabled and start_date <= ? and end_date >= ?")) {
1✔
487
                var today = Timestamp.from(new Date().toInstant());
1✔
488
                statement.setTimestamp(1, today);
1✔
489
                statement.setTimestamp(2, today);
1✔
490
                try (var results = statement.executeQuery()) {
1✔
491
                    while (results.next()) {
1✔
492
                        buildBonusFromResultSetAndAddToList(activebonuses, results);
1✔
493
                    }
494
                }
495
            }
496
        } catch (SQLException e) {
1✔
497
            logWarning("Failed to get list of active bonuses", e);
1✔
498
        }
1✔
499

500
        return activebonuses;
1✔
501
    }
502

503
    @Override
504
    public List<Bonus> getAllBonuses() {
505
        var allbonuses = new ArrayList<Bonus>();
1✔
506
        try(var connection = datasource.getConnection()) {
1✔
507
            try(var statement = connection.prepareStatement("select bonus_id, enabled, iconurl, title, description, bonus_factor, start_date, end_date from bonuses")) {
1✔
508
                try (var results = statement.executeQuery()) {
1✔
509
                    while (results.next()) {
1✔
510
                        buildBonusFromResultSetAndAddToList(allbonuses, results);
1✔
511
                    }
512
                }
513
            }
514
        } catch (SQLException e) {
1✔
515
            logWarning("Failed to get list of all bonuses", e);
1✔
516
        }
1✔
517

518
        return allbonuses;
1✔
519
    }
520

521
    @Override
522
    public List<Bonus> createBonus(Bonus newBonus) {
523
        var title = newBonus.title();
1✔
524
        try(var connection = datasource.getConnection()) {
1✔
525
            try(var statement = connection.prepareStatement("insert into bonuses (enabled, iconurl, title, description, bonus_factor, start_date, end_date) values (?, ?, ?, ?, ?, ?, ?)")) {
1✔
526
                statement.setBoolean(1, newBonus.enabled());
1✔
527
                statement.setString(2, newBonus.iconurl());
1✔
528
                statement.setString(3, title);
1✔
529
                statement.setString(4, newBonus.description());
1✔
530
                statement.setDouble(5, newBonus.bonusFactor());
1✔
531
                var startDate = newBonus.startDate() != null ? newBonus.startDate() : new Date();
1✔
532
                statement.setTimestamp(6, Timestamp.from(startDate.toInstant()));
1✔
533
                var endDate = newBonus.endDate() != null ? newBonus.endDate() : new Date();
1✔
534
                statement.setTimestamp(7, Timestamp.from(endDate.toInstant()));
1✔
535
                statement.executeUpdate();
1✔
536
            }
537
        } catch (SQLException e) {
1✔
538
            logWarning(String.format("Failed to add Bonus with title \"%s\"", title), e);
1✔
539
        }
1✔
540

541
        return getAllBonuses();
1✔
542
    }
543

544
    @Override
545
    public List<Bonus> modifyBonus(Bonus updatedBonus) {
546
        var id = updatedBonus.bonusId();
1✔
547
        try(var connection = datasource.getConnection()) {
1✔
548
            try(var statement = connection.prepareStatement("update bonuses set enabled=?, iconurl=?, title=?, description=?, bonus_factor=?, start_date=?, end_date=? where bonus_id=?")) {
1✔
549
                statement.setBoolean(1, updatedBonus.enabled());
1✔
550
                statement.setString(2, updatedBonus.iconurl());
1✔
551
                statement.setString(3, updatedBonus.title());
1✔
552
                statement.setString(4, updatedBonus.description());
1✔
553
                statement.setDouble(5, updatedBonus.bonusFactor());
1✔
554
                statement.setTimestamp(6, Timestamp.from(updatedBonus.startDate().toInstant()));
1✔
555
                statement.setTimestamp(7, Timestamp.from(updatedBonus.endDate().toInstant()));
1✔
556
                statement.setInt(8, id);
1✔
557
                statement.executeUpdate();
1✔
558
            }
559
        } catch (SQLException e) {
1✔
560
            logWarning(String.format("Failed to update Bonus with database id %d", id), e);
1✔
561
        }
1✔
562

563
        return getAllBonuses();
1✔
564
    }
565

566
    @Override
567
    public List<Bonus> deleteBonus(Bonus removedBonus) {
568
        var id = removedBonus.bonusId();
1✔
569
        try(var connection = datasource.getConnection()) {
1✔
570
            try(var statement = connection.prepareStatement("delete from bonuses where bonus_id=?")) {
1✔
571
                statement.setInt(1, id);
1✔
572
                statement.executeUpdate();
1✔
573
            }
574
        } catch (SQLException e) {
1✔
575
            logWarning(String.format("Failed to delete Bonus with database id %d", id), e);
1✔
576
        }
1✔
577

578
        return getAllBonuses();
1✔
579
    }
580

581
    @Override
582
    public Locale defaultLocale() {
583
        return defaultLocale;
1✔
584
    }
585

586
    @Override
587
    public List<LocaleBean> availableLocales() {
588
        return Arrays.asList(Locale.forLanguageTag("nb-NO"), Locale.UK).stream().map(l -> LocaleBean.with().locale(l).build()).toList();
1✔
589
    }
590

591
    @Override
592
    public Map<String, String> displayTexts(Locale locale) {
593
        return transformResourceBundleToMap(locale);
1✔
594
    }
595

596
    private ConcurrentLinkedQueue<Notification> getNotificationQueueForUser(String username) {
597
        return notificationQueues.computeIfAbsent(username, k-> new ConcurrentLinkedQueue<>());
1✔
598
    }
599

600
    double addBonus(double transactionAmount) {
601
        var activebonuses = getActiveBonuses();
1✔
602
        if (activebonuses.isEmpty()) {
1✔
603
            return transactionAmount;
1✔
604
        }
605

606
        var bonus = activebonuses.stream().mapToDouble(b -> b.bonusFactor() * transactionAmount - transactionAmount).sum();
1✔
607
        return transactionAmount + bonus;
1✔
608
    }
609

610
    void buildBonusFromResultSetAndAddToList(List<Bonus> allbonuses, ResultSet results) throws SQLException {
611
        var bonus = Bonus.with()
1✔
612
            .bonusId(results.getInt("bonus_id"))
1✔
613
            .enabled(results.getBoolean("enabled"))
1✔
614
            .iconurl(results.getString("iconurl"))
1✔
615
            .title(results.getString("title"))
1✔
616
            .description(results.getString("description"))
1✔
617
            .bonusFactor(results.getDouble("bonus_factor"))
1✔
618
            .startDate(Date.from(results.getTimestamp("start_date").toInstant()))
1✔
619
            .endDate(Date.from(results.getTimestamp("end_date").toInstant()))
1✔
620
            .build();
1✔
621
        allbonuses.add(bonus);
1✔
622
    }
1✔
623

624
    static boolean passwordsEqualsAndNotEmpty(PasswordsWithUser passwords) {
625
        if (passwords.password() == null || passwords.password().isEmpty()) {
1✔
626
            return false;
1✔
627
        }
628

629
        return passwords.password().equals(passwords.password2());
1✔
630
    }
631

632
    static StringBuilder joinIds(List<Integer> ids) {
633
        var commaList = new StringBuilder();
1✔
634
        if (ids == null) {
1✔
635
            return commaList;
1✔
636
        }
637

638
        var iterator = ids.iterator();
1✔
639
        if (!iterator.hasNext()) {
1✔
640
            return commaList; // Return an empty string builder instead of a null
1✔
641
        }
642

643
        commaList.append(iterator.next());
1✔
644
        while(iterator.hasNext()) {
1✔
645
            commaList.append(", ").append(iterator.next());
1✔
646
        }
647

648
        return commaList;
1✔
649
    }
650

651
    static boolean hasUserWithNonEmptyUsername(PasswordsWithUser passwords) {
652
        var user = passwords.user();
1✔
653
        if (user == null) {
1✔
654
            return false;
1✔
655
        }
656

657
        var username = user.username();
1✔
658
        if (username == null) {
1✔
659
            return false;
1✔
660
        }
661

662
        return !username.isEmpty();
1✔
663
    }
664

665
    private static void trySettingPreparedStatementParameterThatMayNotBePresent(PreparedStatement statement, int parameterId, int parameterValue) {
666
        try {
667
            statement.setInt(parameterId, parameterValue);
1✔
668
        } catch(SQLException e) {
1✔
669
            // Oops! The parameter wasn't present!
670
            // Continue as if nothing happened
671
        }
1✔
672
    }
1✔
673

674
    private void logError(String message, Exception e) {
675
        logger.error(message, e);
1✔
676
    }
1✔
677

678
    private void logWarning(String message, Exception e) {
679
        logger.warn(message, e);
1✔
680
    }
1✔
681

682
    /**
683
     * Hack!
684
     * Because of the sum() column of accounts_view, accounts without transactions
685
     * won't appear in the accounts list, so all accounts are created with a
686
     * payment of 0 kroner.
687
     * @param username Used as the key to do the update to the account
688
     * @return the update status
689
     */
690
    int addDummyPaymentToAccountSoThatAccountWillAppearInAccountsView(String username) {
691
        try(var connection = datasource.getConnection()) {
1✔
692
            try(var statement = connection.prepareStatement(getResourceAsString("/sql/query/insert_empty_payment_in_account_keyed_by_username.sql"))) {
1✔
693
                statement.setString(1, username);
1✔
UNCOV
694
                return statement.executeUpdate();
×
695
            }
696
        } catch (SQLException e) {
1✔
697
            logError("Failed to set prepared statement argument", e);
1✔
698
        }
699

700
        return -1;
1✔
701
    }
702

703
    String getResourceAsString(String resourceName) {
704
        var resource = new ByteArrayOutputStream();
1✔
705
        var buffer = new byte[1024];
1✔
706
        int length;
707
        try(var resourceStream = getClass().getResourceAsStream(resourceName)) {
1✔
708
            while ((length = resourceStream.read(buffer)) != -1) {
1✔
709
                resource.write(buffer, 0, length);
1✔
710
            }
711

712
            return resource.toString(StandardCharsets.UTF_8);
1✔
713
        } catch (Exception e) {
1✔
714
            logError("Error getting resource \"" + resource + "\" from the classpath", e);
1✔
715
        }
716

717
        return null;
1✔
718
    }
719

720
    public Account mapAccount(ResultSet results) throws SQLException {
721
        var username = results.getString(UkelonnServiceProvider.USERNAME);
1✔
722
        try {
723
            var user = useradmin.getUser(username);
1✔
724
            return Account.with()
1✔
725
                .accountid(results.getInt("account_id"))
1✔
726
                .username(username)
1✔
727
                .firstName(user.firstname())
1✔
728
                .lastName(user.lastname())
1✔
729
                .balance(results.getDouble("balance"))
1✔
730
                .build();
1✔
731
        } catch (AuthserviceException e) {
1✔
732
            logWarning(String.format("No authservice user for username \"%s\" when fetching account", username), e);
1✔
733
            return Account.with()
1✔
734
                .accountid(results.getInt("account_id"))
1✔
735
                .username(username)
1✔
736
                .balance(results.getDouble("balance"))
1✔
737
                .build();
1✔
738
        }
739
    }
740

741
    private void addRolesIfNotPresent() {
742
        var ukelonnadmin = addRoleIfNotPresent(UKELONNADMIN_ROLE, "Administrator av applikasjonen ukelonn");
1✔
743
        addRoleIfNotPresent(UKELONNUSER_ROLE, "Bruker av applikasjonen ukelonn");
1✔
744
        addAdminroleToUserAdmin(ukelonnadmin);
1✔
745
    }
1✔
746

747
    Optional<Role> addRoleIfNotPresent(String rolename, String description) {
748
        var roles = useradmin.getRoles();
1✔
749
        var existingRole = roles.stream().filter(r -> rolename.equals(r.rolename())).findFirst();
1✔
750
        if (!existingRole.isPresent()) {
1✔
751
            roles = useradmin.addRole(Role.with().rolename(rolename).description(description).build());
1✔
752
            return roles.stream().filter(r -> rolename.equals(r.rolename())).findFirst();
1✔
753
        }
754

755
        return existingRole;
1✔
756
    }
757

758
    void addAdminroleToUserAdmin(Optional<Role> ukelonnadmin) {
759
        if (ukelonnadmin.isPresent()) {
1✔
760
            try {
761
                var admin = useradmin.getUser("admin");
1✔
762
                var roles = useradmin.getRolesForUser("admin");
1✔
763
                if (roles.stream().noneMatch(r -> ukelonnadmin.get().equals(r))) {
1✔
764
                    useradmin.addUserRoles(UserRoles.with().user(admin).roles(Arrays.asList(ukelonnadmin.get())).build());
1✔
765
                }
766
            } catch (AuthserviceException e) {
1✔
767
                // No admin user, skip and continue
768
            }
1✔
769
        }
770
    }
1✔
771

772
    static Transaction mapTransaction(ResultSet resultset) throws SQLException {
773
        return Transaction.with()
1✔
774
            .id(resultset.getInt("transaction_id"))
1✔
775
            .transactionType(mapTransactionType(resultset))
1✔
776
            .transactionTime(resultset.getTimestamp("transaction_time"))
1✔
777
            .transactionAmount(resultset.getDouble("transaction_amount"))
1✔
778
            .paidOut(resultset.getBoolean("paid_out"))
1✔
779
            .build();
1✔
780
    }
781

782
    static List<Transaction> makePaymentAmountsPositive(List<Transaction> payments) {
783
        var paymentsWithPositiveAmounts = new ArrayList<Transaction>(payments.size());
1✔
784
        for (var payment : payments) {
1✔
785
            var amount = Math.abs(payment.transactionAmount());
1✔
786
            paymentsWithPositiveAmounts.add(Transaction.with(payment).transactionAmount(amount).build());
1✔
787
        }
1✔
788

789
        return paymentsWithPositiveAmounts;
1✔
790
    }
791

792
    static TransactionType mapTransactionType(ResultSet resultset) throws SQLException {
793
        return TransactionType.with()
1✔
794
            .id(resultset.getInt("transaction_type_id"))
1✔
795
            .transactionTypeName(resultset.getString("transaction_type_name"))
1✔
796
            .transactionAmount(resultset.getDouble("transaction_amount"))
1✔
797
            .transactionIsWork(resultset.getBoolean("transaction_is_work"))
1✔
798
            .transactionIsWagePayment(resultset.getBoolean("transaction_is_wage_payment"))
1✔
799
            .build();
1✔
800
    }
801

802
    Map<String, String> transformResourceBundleToMap(Locale locale) {
803
        var map = new HashMap<String, String>();
1✔
804
        var bundle = ResourceBundle.getBundle(RESOURCES_BASENAME, locale);
1✔
805
        var keys = bundle.getKeys();
1✔
806
        while(keys.hasMoreElements()) {
1✔
807
            String key = keys.nextElement();
1✔
808
            map.put(key, bundle.getString(key));
1✔
809
        }
1✔
810

811
        return map;
1✔
812
    }
813

814
}
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