39. Найти и исправить проблемы в расчёте счетов за приёмы
Условие задачи:
Дан код формирования счетов за приёмы пациентов.
В текущей реализации есть несколько проблем:
- некоторые пациенты пропадают из результата, особенно если в данных отсутствует скидка или она приходит в неожиданном формате;
- в счетах могут появляться отрицательные суммы;
- изменение цены приёма через
updatePrice()фактически ничего не меняет; - денежные суммы выводятся с большим количеством знаков после запятой;
- отменённые приёмы попадают в счёт.
Static-блок используется только для эмуляции данных, которые в реальном приложении могут приходить из БД или внешней системы.
Код:
public class AppointmentBilling {
static List<Map<String, Object>> appointments = new ArrayList<>();
static {
Map<String, Object> a1 = new HashMap<>();
a1.put("id", 1);
a1.put("doctor", "Иванов");
a1.put("patient", "Волков");
a1.put("base_price", 2500.0);
a1.put("discount_pct", 0.10);
a1.put("status", "completed");
appointments.add(a1);
Map<String, Object> a2 = new HashMap<>();
a2.put("id", 2);
a2.put("doctor", "Петрова");
a2.put("patient", "Зайцева");
a2.put("base_price", -1500.0);
a2.put("discount_pct", 0.05);
a2.put("status", "completed");
appointments.add(a2);
Map<String, Object> a3 = new HashMap<>();
a3.put("id", 3);
a3.put("doctor", "Сидоров");
a3.put("patient", "Лисицын");
a3.put("base_price", 3000.0);
a3.put("discount_pct", "none");
a3.put("status", "completed");
appointments.add(a3);
Map<String, Object> a4 = new HashMap<>();
a4.put("id", 4);
a4.put("doctor", "Кузнецова");
a4.put("patient", "Медведева");
a4.put("base_price", 4500.50);
a4.put("discount_pct", 0.15);
a4.put("status", "canceled");
appointments.add(a4);
}
static double calcFinalPrice(Map<String, Object> apt) {
double base =
((Number) apt.get("base_price")).doubleValue();
double discount =
((Number) apt.get("discount_pct")).doubleValue();
return base - (base * discount);
}
static List<Map<String, Object>> generateInvoice() {
List<Map<String, Object>> invoice = new ArrayList<>();
for (Map<String, Object> apt : appointments) {
try {
double price = calcFinalPrice(apt);
Map<String, Object> entry = new HashMap<>();
entry.put("patient", apt.get("patient"));
entry.put("doctor", apt.get("doctor"));
entry.put("price", price);
invoice.add(entry);
} catch (Exception e) {
continue;
}
}
return invoice;
}
static void updatePrice(int id, double newPrice) {
for (Map<String, Object> apt : appointments) {
if ((int) apt.get("id") == id) {
apt.get("base_price");
break;
}
}
}
}
Спойлеры к решению
Подсказки
catch (Exception) вместе с continue полностью скрывает ошибку и приводит к исчезновению записи из результата.💡 Значение
discount_pct не всегда является Number: в примере встречается строка "none".💡 Для денежных расчётов лучше использовать
BigDecimal, а не double.💡 Отрицательная базовая стоимость должна валидироваться до формирования счёта.
💡 Для денег нужно явно задать округление до двух знаков через
setScale().💡 Перед формированием счёта необходимо проверить
status.💡 В
updatePrice() нужно изменить значение через put(), а не просто получить его через get().Решение
Проблемы:
discount_pct = "none"приводит кClassCastException;catch (Exception)скрывает эту ошибку, поэтому пациент просто исчезает из результата;- отсутствующая скидка у новой записи также может привести к ошибке;
- отрицательный
base_priceприводит к отрицательному счёту; - для денег используется
double; - отсутствует явное округление до двух знаков;
- отменённые приёмы никак не фильтруются;
updatePrice()вызываетget(), но не изменяет значение;- использование
Map<String, Object>позволяет получить данные неожиданного типа.
Исправленный вариант:
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class AppointmentBilling {
static List<Map<String, Object>> appointments = new ArrayList<>();
static {
Map<String, Object> a1 = new HashMap<>();
a1.put("id", 1);
a1.put("doctor", "Иванов");
a1.put("patient", "Волков");
a1.put("base_price", 2500.0);
a1.put("discount_pct", 0.10);
a1.put("status", "completed");
appointments.add(a1);
Map<String, Object> a2 = new HashMap<>();
a2.put("id", 2);
a2.put("doctor", "Петрова");
a2.put("patient", "Зайцева");
a2.put("base_price", -1500.0);
a2.put("discount_pct", 0.05);
a2.put("status", "completed");
appointments.add(a2);
Map<String, Object> a3 = new HashMap<>();
a3.put("id", 3);
a3.put("doctor", "Сидоров");
a3.put("patient", "Лисицын");
a3.put("base_price", 3000.0);
a3.put("discount_pct", "none");
a3.put("status", "completed");
appointments.add(a3);
Map<String, Object> a4 = new HashMap<>();
a4.put("id", 4);
a4.put("doctor", "Кузнецова");
a4.put("patient", "Медведева");
a4.put("base_price", 4500.50);
a4.put("discount_pct", 0.15);
a4.put("status", "canceled");
appointments.add(a4);
}
static BigDecimal calcFinalPrice(
Map<String, Object> apt
) {
BigDecimal base = toDecimal(
apt.get("base_price"),
"base_price"
);
if (base.signum() < 0) {
throw new IllegalArgumentException(
"base_price must not be negative"
);
}
BigDecimal discount = parseDiscount(
apt.get("discount_pct")
);
if (discount.compareTo(BigDecimal.ZERO) < 0
|| discount.compareTo(BigDecimal.ONE) > 0) {
throw new IllegalArgumentException(
"discount_pct must be between 0 and 1"
);
}
return base
.multiply(BigDecimal.ONE.subtract(discount))
.setScale(2, RoundingMode.HALF_UP);
}
static BigDecimal parseDiscount(Object value) {
if (value == null) {
return BigDecimal.ZERO;
}
if (value instanceof String string) {
if (string.isBlank()
|| string.equalsIgnoreCase("none")) {
return BigDecimal.ZERO;
}
}
return toDecimal(value, "discount_pct");
}
static BigDecimal toDecimal(
Object value,
String field
) {
if (value == null) {
throw new IllegalArgumentException(
field + " must not be null"
);
}
if (value instanceof BigDecimal decimal) {
return decimal;
}
if (value instanceof Number number) {
return new BigDecimal(number.toString());
}
if (value instanceof String string) {
try {
return new BigDecimal(string);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
"Invalid " + field,
e
);
}
}
throw new IllegalArgumentException(
"Invalid type for " + field
);
}
static List<Map<String, Object>> generateInvoice() {
List<Map<String, Object>> invoice =
new ArrayList<>();
for (Map<String, Object> apt : appointments) {
if (!"completed".equals(apt.get("status"))) {
continue;
}
Map<String, Object> entry =
new HashMap<>();
entry.put("patient", apt.get("patient"));
entry.put("doctor", apt.get("doctor"));
try {
BigDecimal price =
calcFinalPrice(apt);
entry.put("price", price);
} catch (RuntimeException e) {
entry.put("error", e.getMessage());
}
invoice.add(entry);
}
return invoice;
}
static void updatePrice(
int id,
BigDecimal newPrice
) {
if (newPrice == null
|| newPrice.signum() < 0) {
throw new IllegalArgumentException(
"Price must not be negative"
);
}
for (Map<String, Object> apt : appointments) {
if (((Number) apt.get("id")).intValue() == id) {
apt.put("base_price", newPrice);
return;
}
}
throw new IllegalArgumentException(
"Appointment not found: " + id
);
}
public static void main(String[] args) {
System.out.println("Счета за приёмы:");
generateInvoice().forEach(entry -> {
if (entry.containsKey("error")) {
System.out.println(
entry.get("patient")
+ " -> "
+ entry.get("doctor")
+ ": ошибка — "
+ entry.get("error")
);
} else {
System.out.println(
entry.get("patient")
+ " -> "
+ entry.get("doctor")
+ ": "
+ entry.get("price")
+ " руб."
);
}
});
}
}
1. Почему пропадали пациенты
Для пациента Лисицына приходит:
discount_pct = "none"
а исходный код выполняет:
((Number) apt.get("discount_pct")).doubleValue();
Строку нельзя привести к Number, поэтому возникает ClassCastException.
После этого:
catch (Exception e) {
continue;
}
полностью скрывает ошибку и переходит к следующему пациенту.
В исправленном варианте значение:
none
интерпретируется как отсутствие скидки:
return BigDecimal.ZERO;
То же самое происходит, если скидка отсутствует:
discount_pct = null
2. Отрицательные суммы
В исходных данных:
base_price = -1500.0
Такое значение не должно автоматически превращаться в отрицательный счёт.
Поэтому выполняется явная проверка:
if (base.signum() < 0) {
throw new IllegalArgumentException(
"base_price must not be negative"
);
}
При этом пациент не исчезает полностью: в результате сохраняется запись с информацией об ошибке.
3. Изменение цены
Исходный код:
apt.get("base_price");
только получает значение и ничего не изменяет.
Необходимо использовать:
apt.put("base_price", newPrice);
После этого следующий вызов generateInvoice() рассчитает счёт уже с новой стоимостью.
4. Округление
Для денежных значений используется BigDecimal:
BigDecimal
а окончательный результат округляется:
.setScale(2, RoundingMode.HALF_UP);
Поэтому вместо:
2234.56789
получим:
2234.57
5. Отменённые приёмы
До расчёта стоимости проверяется статус:
if (!"completed".equals(apt.get("status"))) {
continue;
}
Поэтому:
canceled
scheduled
не попадут в счёт.
Для исходных данных результат будет примерно таким:
Счета за приёмы:
Волков -> Иванов: 2250.00 руб.
Зайцева -> Петрова: ошибка — base_price must not be negative
Лисицын -> Сидоров: 3000.00 руб.
Приём Медведевой не выводится, поскольку его статус:
canceled
Главные исправления — не скрывать ошибки через catch (Exception), корректно обрабатывать отсутствующую скидку, валидировать отрицательные цены, использовать BigDecimal, округлять денежные значения до двух знаков, фильтровать статус и действительно изменять цену через put().