""" 营业收费计算逻辑单元测试 覆盖计费逻辑、折扣计算和滞纳金计算 """ import unittest from unittest.mock import Mock, patch from datetime import datetime, timedelta, date import json from src.billing.models import Bill, Payment, Tariff, DiscountRule from src.billing.services import BillingService, PaymentService, DiscountService class TestTariff(unittest.TestCase): """费率模型测试""" def setUp(self): self.tariff_data = { "tariff_id": "water_tier_1", "name": "第一阶梯水价", "description": "居民用水第一阶梯", "unit_price": 3.5, # 元/立方米 "currency": "CNY", "effective_date": "2026-01-01", "expiry_date": "2026-12-31", "consumption_tiers": [ {"min": 0, "max": 12, "price": 3.5}, {"min": 12, "max": 24, "price": 5.0}, {"min": 24, "max": float('inf'), "price": 7.0} ] } def test_tariff_creation(self): """测试费率创建""" tariff = Tariff(self.tariff_data) self.assertEqual(tariff.tariff_id, self.tariff_data["tariff_id"]) self.assertEqual(tariff.unit_price, 3.5) self.assertEqual(len(tariff.consumption_tiers), 3) # 验证阶梯价格 self.assertEqual(tariff.get_price_for_consumption(10), 3.5) # 第一阶梯 self.assertEqual(tariff.get_price_for_consumption(15), 5.0) # 第二阶梯 self.assertEqual(tariff.get_price_for_consumption(30), 7.0) # 第三阶梯 def test_tariff_validation(self): """测试费率验证""" tariff = Tariff(self.tariff_data) result = tariff.validate() self.assertTrue(result["valid"]) # 测试无效费率(负价格) invalid_tariff_data = self.tariff_data.copy() invalid_tariff_data["unit_price"] = -1.0 invalid_tariff = Tariff(invalid_tariff_data) result = invalid_tariff.validate() self.assertFalse(result["valid"]) def test_tariff_effective_date(self): """测试费率有效期""" tariff = Tariff(self.tariff_data) # 在有效期内 current_date = date(2026, 6, 16) self.assertTrue(tariff.is_effective_on(current_date)) # 在有效期前 future_date = date(2027, 1, 1) self.assertFalse(tariff.is_effective_on(future_date)) class TestBill(unittest.TestCase): """账单模型测试""" def setUp(self): self.bill_data = { "bill_id": "bill_001", "customer_id": "customer_001", "account_number": "ACC123456", "billing_period": { "start_date": "2026-05-01", "end_date": "2026-05-31" }, "consumption": { "water_consumption": 15.5, # 立方米 "sewage_fee": 2.3, "other_fees": 10.0 }, "charges": [], "due_date": "2026-06-15", "status": "pending", "created_at": "2026-05-31T23:59:59Z" } def test_bill_creation(self): """测试账单创建""" bill = Bill(self.bill_data) self.assertEqual(bill.bill_id, self.bill_data["bill_id"]) self.assertEqual(bill.customer_id, self.bill_data["customer_id"]) self.assertEqual(bill.status, "pending") self.assertEqual(bill.consumption["water_consumption"], 15.5) # 验证日期 self.assertTrue(datetime.fromisoformat(bill.billing_period["start_date"])) self.assertTrue(datetime.fromisoformat(bill.billing_period["end_date"])) def test_bill_calculation(self): """测试账单计算""" bill = Bill(self.bill_data) tariff = Tariff(self.tariff_data) # 计算水费 water_charge = bill.calculate_water_charge(tariff) expected_water_fee = 12 * 3.5 + (15.5 - 12) * 5.0 # 阶梯计算 self.assertAlmostEqual(water_charge, expected_water_fee, places=2) # 计算总费用 total_charge = bill.calculate_total_charge(tariff) expected_total = expected_water_fee + 2.3 + 10.0 self.assertAlmostEqual(total_charge, expected_total, places=2) def test_bill_status_transitions(self): """测试账单状态转换""" bill = Bill(self.bill_data) # 从pending到issued bill.issue_bill() self.assertEqual(bill.status, "issued") self.assertIsNotNone(bill.issued_at) # 从issued到overdue due_date = datetime.strptime(bill.due_date, "%Y-%m-%d").date() overdue_date = due_date + timedelta(days=1) bill.mark_as_overdue(overdue_date) self.assertEqual(bill.status, "overdue") # 从overdue到paid bill.mark_as_paid() self.assertEqual(bill.status, "paid") class TestBillingService(unittest.TestCase): """计费服务测试""" def setUp(self): self.billing_service = BillingService() @patch('src.billing.services.Tariff') @patch('src.billing.services.Bill') def test_generate_monthly_bill(self, mock_bill_class, mock_tariff_class): """测试生成月度账单""" customer_id = "customer_001" period = {"start_date": "2026-05-01", "end_date": "2026-05-31"} consumption_data = {"water_consumption": 15.5} mock_tariff = Mock() mock_tariff.get_price_for_consumption.return_value = 3.5 mock_tariff_class.get_active_tariff.return_value = mock_tariff mock_bill = Mock() mock_bill.calculate_total_charge.return_value = 65.25 mock_bill_class.return_value = mock_bill result = self.billing_service.generate_monthly_bill(customer_id, period, consumption_data) self.assertTrue(result["success"]) self.assertEqual(result["total_amount"], 65.25) mock_bill_class.assert_called_once() @patch('src.billing.services.Tariff') def test_calculate_late_fee(self, mock_tariff): """测试滞纳金计算""" base_amount = 100.0 due_date = date(2026, 6, 1) current_date = date(2026, 6, 16) # 15天滞纳 late_fee_rate = 0.002 # 每天0.2% late_fee = self.billing_service.calculate_late_fee( base_amount, due_date, current_date, late_fee_rate ) expected_late_fee = base_amount * late_fee_rate * 15 self.assertAlmostEqual(late_fee, expected_late_fee, places=2) @patch('src.billing.services.Bill') def test_get_customer_bills(self, mock_bill): """测试获取客户账单""" customer_id = "customer_001" mock_bill.query.return_value = [ {"bill_id": "bill_001", "amount": 100.0, "status": "paid"}, {"bill_id": "bill_002", "amount": 150.0, "status": "pending"} ] bills = self.billing_service.get_customer_bills(customer_id) self.assertEqual(len(bills), 2) mock_bill.query.assert_called_with(customer_id=customer_id) def test_billing_validation(self): """测试计费验证""" bill_data = { "customer_id": "customer_001", "consumption": {"water_consumption": -5.0} # 负消费量 } result = self.billing_service.validate_billing_data(bill_data) self.assertFalse(result["valid"]) self.assertIn("Negative consumption", result["errors"]) class TestDiscountService(unittest.TestCase): """折扣服务测试""" def setUp(self): self.discount_service = DiscountService() def test_early_payment_discount(self): """测试提前支付折扣""" bill_amount = 1000.0 due_date = date(2026, 6, 30) payment_date = date(2026, 6, 15) # 提前15天 discount_rate = 0.05 # 5%折扣 discount = self.discount_service.calculate_early_payment_discount( bill_amount, due_date, payment_date, discount_rate ) expected_discount = bill_amount * discount_rate self.assertEqual(discount, expected_discount) # 计算最终金额 final_amount = self.discount_service.apply_discount(bill_amount, discount) self.assertEqual(final_amount, bill_amount - expected_discount) def test_bulk_discount(self): """测试批量支付折扣""" customer_id = "customer_001" bills = [ {"bill_id": "bill_001", "amount": 500.0}, {"bill_id": "bill_002", "amount": 1000.0}, {"bill_id": "bill_003", "amount": 2000.0} ] # 批量总金额超过3000,享受3%折扣 bulk_amount = sum(bill["amount"] for bill in bills) discount = self.discount_service.calculate_bulk_discount(customer_id, bills, 3000, 0.03) expected_discount = bulk_amount * 0.03 self.assertEqual(discount, expected_discount) def test_seasonal_discount(self): """测试季节性折扣""" current_date = date(2026, 6, 16) # 夏季 bill_amount = 500.0 seasonal_discount_rate = 0.1 # 夏季10%折扣 discount = self.discount_service.calculate_seasonal_discount(current_date, bill_amount, seasonal_discount_rate) expected_discount = bill_amount * seasonal_discount_rate self.assertEqual(discount, expected_discount) def test_discount_combination(self): """测试折扣组合""" base_amount = 1000.0 # 提前支付折扣 early_discount = 50.0 # 季节性折扣 seasonal_discount = 100.0 # 应用折扣(通常不叠加,取最大值) final_discount = self.discount_service.apply_multiple_discounts( base_amount, [early_discount, seasonal_discount] ) self.assertEqual(final_discount, seasonal_discount) # 取最大值 final_amount = base_amount - final_discount self.assertEqual(final_amount, 900.0) class TestPaymentService(unittest.TestCase): """支付服务测试""" def setUp(self): self.payment_service = PaymentService() @patch('src.billing.services.Payment') def test_process_payment(self, mock_payment_class): """测试处理支付""" payment_data = { "bill_id": "bill_001", "amount": 500.0, "payment_method": "bank_transfer", "transaction_id": "txn_001" } mock_payment = Mock() mock_payment.process.return_value = {"success": True, "payment_id": "payment_001"} mock_payment_class.return_value = mock_payment result = self.payment_service.process_payment(payment_data) self.assertTrue(result["success"]) self.assertEqual(result["payment_id"], "payment_001") mock_payment.process.assert_called_once() def test_payment_validation(self): """测试支付验证""" payment_data = { "bill_id": "bill_001", "amount": -100.0, # 负金额 "payment_method": "invalid_method" } result = self.payment_service.validate_payment(payment_data) self.assertFalse(result["valid"]) self.assertIn("Negative amount", result["errors"]) self.assertIn("Invalid payment method", result["errors"]) @patch('src.billing.services.Bill') def test_payment_reconciliation(self, mock_bill): """测试支付对账""" payment_data = { "bill_id": "bill_001", "amount": 500.0, "transaction_id": "txn_001" } mock_bill.get.return_value = { "bill_id": "bill_001", "amount_due": 500.0, "status": "pending" } result = self.payment_service.reconcile_payment(payment_data) self.assertTrue(result["success"]) self.assertEqual(result["status"], "paid") mock_bill.update_status.assert_called_once_with("bill_001", "paid") if __name__ == '__main__': unittest.main()