Coverage for website/payments/tests/test_models.py: 99.67%

295 statements  

« prev     ^ index     » next       coverage.py v7.6.7, created at 2025-08-14 10:31 +0000

1import datetime 

2from decimal import Decimal 

3from unittest.mock import PropertyMock, patch 

4 

5from django.core.exceptions import ValidationError 

6from django.db.models.deletion import ProtectedError 

7from django.test import TestCase, override_settings 

8from django.utils import timezone 

9 

10from freezegun import freeze_time 

11 

12from events.models import Event, EventRegistration 

13from payments import services 

14from payments.models import BankAccount, Batch, Payment, PaymentUser, validate_not_zero 

15from payments.tests.__mocks__ import MockModel, MockPayable 

16from sales import payables 

17 

18 

19@override_settings(SUSPEND_SIGNALS=True, THALIA_PAY_ENABLED_PAYMENT_METHOD=True) 

20class PaymentTest(TestCase): 

21 """Tests for the Payment model.""" 

22 

23 fixtures = ["members.json", "bank_accounts.json"] 

24 

25 @classmethod 

26 def setUpTestData(cls): 

27 cls.member = PaymentUser.objects.filter(last_name="Wiggers").first() 

28 cls.payment = Payment.objects.create( 

29 amount=10, paid_by=cls.member, processed_by=cls.member, type=Payment.CASH 

30 ) 

31 cls.batch = Batch.objects.create() 

32 

33 def setUp(self) -> None: 

34 self.batch.processed = False 

35 self.batch.save() 

36 

37 def test_get_admin_url(self): 

38 """Tests that the right admin url is returned.""" 

39 self.assertEqual( 

40 self.payment.get_admin_url(), 

41 f"/admin/payments/payment/{self.payment.pk}/change/", 

42 ) 

43 

44 def test_add_payment_from_processed_batch_to_new_batch(self) -> None: 

45 """Test that a payment that is in a processed batch cannot be added to another batch.""" 

46 self.payment.type = Payment.TPAY 

47 self.payment.batch = self.batch 

48 self.payment.save() 

49 self.batch.processed = True 

50 self.batch.save() 

51 

52 b = Batch.objects.create() 

53 self.payment.batch = b 

54 with self.assertRaises(ValidationError): 

55 self.payment.save() 

56 

57 def test_delete_payer_raises_protectederror(self): 

58 with self.assertRaises(ProtectedError): 

59 self.member.delete() 

60 

61 def test_clean(self): 

62 """Tests the model clean functionality.""" 

63 with self.subTest("Block Thalia Pay creation when it is disabled for user"): 

64 with override_settings(THALIA_PAY_ENABLED_PAYMENT_METHOD=False): 

65 self.payment.type = Payment.TPAY 

66 self.payment.batch = self.batch 

67 with self.assertRaises(ValidationError): 

68 self.payment.clean() 

69 

70 self.payment.type = Payment.TPAY 

71 self.payment.batch = self.batch 

72 self.payment.clean() 

73 

74 with self.subTest("Zero euro payments cannot exist"): 

75 self.payment.amount = 0 

76 with self.assertRaises(ValidationError): 

77 self.payment.clean() 

78 self.payment.refresh_from_db() 

79 

80 with self.subTest("Test that only Thalia Pay can be added to a batch"): 

81 for payment_type in [Payment.CASH, Payment.CARD, Payment.WIRE]: 

82 self.payment.type = payment_type 

83 self.payment.batch = self.batch 

84 with self.assertRaises(ValidationError): 

85 self.payment.clean() 

86 

87 for payment_type in [Payment.CASH, Payment.CARD, Payment.WIRE]: 

88 self.payment.type = payment_type 

89 self.payment.batch = None 

90 self.payment.clean() 

91 

92 self.payment.type = Payment.TPAY 

93 self.payment.batch = self.batch 

94 self.payment.clean() 

95 

96 with self.subTest("Block payment change with when batch is processed"): 

97 batch = Batch.objects.create() 

98 payment = Payment.objects.create( 

99 amount=10, 

100 paid_by=self.member, 

101 processed_by=self.member, 

102 batch=batch, 

103 type=Payment.TPAY, 

104 ) 

105 batch.processed = True 

106 batch.save() 

107 payment.amount = 5 

108 with self.assertRaisesMessage( 

109 ValidationError, 

110 "Cannot change a payment that is part of a processed batch", 

111 ): 

112 payment.clean() 

113 

114 with self.subTest("Block payment connect with processed batch"): 

115 payment = Payment.objects.create( 

116 amount=10, 

117 paid_by=self.member, 

118 processed_by=self.member, 

119 type=Payment.TPAY, 

120 ) 

121 payment.batch = Batch.objects.create(processed=True) 

122 with self.assertRaisesMessage( 

123 ValidationError, "Cannot add a payment to a processed batch" 

124 ): 

125 payment.clean() 

126 

127 with self.subTest("Thalia Pay payments must have a payer"): 

128 with self.assertRaises(ValidationError): 

129 Payment.objects.create(amount=10, type=Payment.TPAY) 

130 payment.clean() 

131 

132 def test_str(self) -> None: 

133 """Tests that the output is a description with the amount.""" 

134 self.assertEqual("Payment of 10.00", str(self.payment)) 

135 

136 def test_payment_amount(self): 

137 """Test the maximal payment amount.""" 

138 with self.subTest("Payments max. amount is 999999.99"): 

139 Payment.objects.create( 

140 type=Payment.WIRE, 

141 paid_by=self.member, 

142 processed_by=self.member, 

143 amount=999999.99, 

144 ) 

145 

146 with self.subTest("Negative payments are actually allowed"): 

147 Payment.objects.create( 

148 type=Payment.WIRE, 

149 paid_by=self.member, 

150 processed_by=self.member, 

151 amount=-999999.99, 

152 ) 

153 

154 with self.subTest("Payments can't have an amount of higher than 1000000"): 

155 with self.assertRaises(ValidationError): 

156 p = Payment( 

157 type=Payment.WIRE, 

158 paid_by=self.member, 

159 processed_by=self.member, 

160 amount=1000000, 

161 ) 

162 p.full_clean() 

163 

164 with self.subTest("Payments of amount 0 are not allowed"): 

165 with self.assertRaises(ValidationError): 

166 Payment.objects.create( 

167 type=Payment.WIRE, 

168 paid_by=self.member, 

169 processed_by=self.member, 

170 amount=0, 

171 ) 

172 

173 def test_validator(self): 

174 validate_not_zero(1) 

175 validate_not_zero(-1) 

176 validate_not_zero(0.01) 

177 validate_not_zero(-0.01) 

178 validate_not_zero(1000000) 

179 validate_not_zero(-1000000) 

180 validate_not_zero(10000000) 

181 validate_not_zero(-10000000) 

182 with self.assertRaises(ValidationError): 

183 validate_not_zero(0) 

184 

185 def test_payable_object(self): 

186 """Test that the payable object is correctly returned.""" 

187 event = Event.objects.create( 

188 title="testevent", 

189 description="desc", 

190 start=timezone.now(), 

191 end=(timezone.now() + datetime.timedelta(hours=1)), 

192 location="test location", 

193 map_location="test map location", 

194 price=1.00, 

195 fine=0.00, 

196 ) 

197 registration = EventRegistration.objects.create(event=event, member=self.member) 

198 services.create_payment(registration, self.member, Payment.WIRE) 

199 registration.refresh_from_db() 

200 payment_id = registration.payment.id 

201 

202 with self.subTest("payable_object property without prefetch"): 

203 with self.assertNumQueries(1): 

204 payment = Payment.objects.get(id=payment_id) 

205 

206 # This may perform multiple queries until the payable model is found. 

207 # The number of queries depends on the order of the registered payables. 

208 num_queries = list(Payment.get_payable_prefetches()).index( 

209 "events_registration" 

210 ) 

211 with self.assertNumQueries(num_queries + 1): 

212 self.assertEqual(payment.payable_object, registration) 

213 

214 # Getting the property once more should not do any more queries. 

215 with self.assertNumQueries(0): 

216 self.assertEqual(payment.payable_object, registration) 

217 

218 with self.subTest("payable_object property with prefetch"): 

219 payment = Payment.objects.prefetch_related( 

220 *Payment.get_payable_prefetches() 

221 ).get(id=payment_id) 

222 

223 # When the prefetch has been done, the property should not perform queries. 

224 with self.assertNumQueries(0): 

225 self.assertEqual(payment.payable_object, registration) 

226 

227 

228@freeze_time("2019-01-01") 

229@override_settings(SUSPEND_SIGNALS=True, THALIA_PAY_ENABLED_PAYMENT_METHOD=True) 

230@patch("payments.models.PaymentUser.tpay_allowed", PropertyMock, True) 

231class BatchModelTest(TestCase): 

232 @classmethod 

233 def setUpTestData(cls) -> None: 

234 cls.user1 = PaymentUser.objects.create( 

235 username="test1", 

236 first_name="Test1", 

237 last_name="Example", 

238 email="test1@example.org", 

239 is_staff=False, 

240 ) 

241 cls.user2 = PaymentUser.objects.create( 

242 username="test2", 

243 first_name="Test2", 

244 last_name="Example", 

245 email="test2@example.org", 

246 is_staff=True, 

247 ) 

248 

249 BankAccount.objects.create( 

250 owner=cls.user1, 

251 initials="J", 

252 last_name="Test", 

253 iban="NL91ABNA0417164300", 

254 mandate_no="11-1", 

255 valid_from=timezone.now().date() - timezone.timedelta(days=5), 

256 signature="base64,png", 

257 ) 

258 

259 def setUp(self): 

260 self.user1.refresh_from_db() 

261 self.user2.refresh_from_db() 

262 

263 def test_start_date_batch(self) -> None: 

264 batch = Batch.objects.create(id=1) 

265 Payment.objects.create( 

266 created_at=timezone.now() + datetime.timedelta(days=1), 

267 type=Payment.TPAY, 

268 amount=37, 

269 paid_by=self.user1, 

270 processed_by=self.user2, 

271 batch=batch, 

272 ) 

273 Payment.objects.create( 

274 created_at=timezone.now(), 

275 type=Payment.TPAY, 

276 amount=36, 

277 paid_by=self.user1, 

278 processed_by=self.user2, 

279 batch=batch, 

280 ) 

281 self.assertEqual(batch.start_date(), timezone.now()) 

282 

283 def test_end_date_batch(self) -> None: 

284 batch = Batch.objects.create(id=1) 

285 Payment.objects.create( 

286 created_at=timezone.now() + datetime.timedelta(days=1), 

287 type=Payment.TPAY, 

288 amount=37, 

289 paid_by=self.user1, 

290 processed_by=self.user2, 

291 batch=batch, 

292 ) 

293 Payment.objects.create( 

294 created_at=timezone.now(), 

295 type=Payment.TPAY, 

296 amount=36, 

297 paid_by=self.user1, 

298 processed_by=self.user2, 

299 batch=batch, 

300 ) 

301 self.assertEqual(batch.end_date(), timezone.now() + datetime.timedelta(days=1)) 

302 

303 def test_description_batch(self) -> None: 

304 batch = Batch.objects.create(id=1) 

305 self.assertEqual( 

306 batch.description, 

307 "Thalia Pay payments for 2019-1", 

308 ) 

309 

310 def test_process_batch(self) -> None: 

311 batch = Batch.objects.create(id=1) 

312 batch.processed = True 

313 batch.save() 

314 self.assertEqual(batch.processing_date, timezone.now()) 

315 

316 def test_total_amount_batch(self) -> None: 

317 batch = Batch.objects.create(id=1) 

318 Payment.objects.create( 

319 created_at=timezone.now() + datetime.timedelta(days=1), 

320 type=Payment.TPAY, 

321 amount=37, 

322 paid_by=self.user1, 

323 processed_by=self.user2, 

324 batch=batch, 

325 ) 

326 Payment.objects.create( 

327 created_at=timezone.now(), 

328 type=Payment.TPAY, 

329 amount=36, 

330 paid_by=self.user1, 

331 processed_by=self.user2, 

332 batch=batch, 

333 ) 

334 self.assertEqual(batch.total_amount(), 36 + 37) 

335 

336 def test_count_batch(self) -> None: 

337 batch = Batch.objects.create(id=1) 

338 Payment.objects.create( 

339 created_at=timezone.now() + datetime.timedelta(days=1), 

340 type=Payment.TPAY, 

341 amount=37, 

342 paid_by=self.user1, 

343 processed_by=self.user2, 

344 batch=batch, 

345 ) 

346 Payment.objects.create( 

347 created_at=timezone.now(), 

348 type=Payment.TPAY, 

349 amount=36, 

350 paid_by=self.user1, 

351 processed_by=self.user2, 

352 batch=batch, 

353 ) 

354 self.assertEqual(batch.payments_count(), 2) 

355 

356 def test_absolute_url(self) -> None: 

357 b1 = Batch.objects.create(id=1) 

358 self.assertEqual("/admin/payments/batch/1/change/", b1.get_absolute_url()) 

359 

360 def test_str(self) -> None: 

361 b1 = Batch.objects.create(id=1) 

362 self.assertEqual("Thalia Pay payments for 2019-1 (not processed)", str(b1)) 

363 b2 = Batch.objects.create(id=2, processed=True) 

364 self.assertEqual("Thalia Pay payments for 2019-1 (processed)", str(b2)) 

365 

366 

367@freeze_time("2019-01-01") 

368@override_settings(SUSPEND_SIGNALS=True, THALIA_PAY_ENABLED_PAYMENT_METHOD=True) 

369class BankAccountTest(TestCase): 

370 """Tests for the BankAccount model.""" 

371 

372 fixtures = ["members.json"] 

373 

374 @classmethod 

375 def setUpTestData(cls) -> None: 

376 cls.member = PaymentUser.objects.filter(last_name="Wiggers").first() 

377 cls.no_mandate = BankAccount.objects.create( 

378 owner=cls.member, initials="J", last_name="Test", iban="NL91ABNA0417164300" 

379 ) 

380 cls.with_mandate = BankAccount.objects.create( 

381 owner=cls.member, 

382 initials="J", 

383 last_name="Test", 

384 iban="NL91ABNA0417164300", 

385 mandate_no="11-1", 

386 valid_from=timezone.now().date() - timezone.timedelta(days=5), 

387 signature="base64,png", 

388 ) 

389 

390 def setUp(self) -> None: 

391 self.no_mandate.refresh_from_db() 

392 self.with_mandate.refresh_from_db() 

393 

394 def test_name(self) -> None: 

395 """Tests that the property returns initials concatenated with last name.""" 

396 self.assertEqual("J Test", self.no_mandate.name) 

397 self.no_mandate.initials = "R" 

398 self.no_mandate.last_name = "Hood" 

399 self.assertEqual("R Hood", self.no_mandate.name) 

400 

401 def test_valid(self) -> None: 

402 """Tests that the property returns the right validity of the mandate.""" 

403 self.assertFalse(self.no_mandate.valid) 

404 self.assertTrue(self.with_mandate.valid) 

405 self.with_mandate.valid_until = timezone.now().date() 

406 self.assertFalse(self.with_mandate.valid) 

407 

408 def test_can_be_revoked(self) -> None: 

409 """ 

410 Tests the correct property value for bank accounts that have unprocessed 

411 or unbatched Thalia Pay payments that hence cannot be revoked by users directly 

412 """ 

413 self.assertTrue(self.with_mandate.can_be_revoked) 

414 self.member.refresh_from_db() 

415 payment = Payment.objects.create( 

416 paid_by=self.member, type=Payment.TPAY, topic="test", amount=3 

417 ) 

418 self.assertFalse(self.with_mandate.can_be_revoked) 

419 batch = Batch.objects.create() 

420 payment.batch = batch 

421 payment.save() 

422 self.assertFalse(self.with_mandate.can_be_revoked) 

423 batch.processed = True 

424 batch.save() 

425 self.assertTrue(self.with_mandate.can_be_revoked) 

426 

427 def test_str(self) -> None: 

428 """ 

429 Tests that the output is the IBAN concatenated 

430 with the name of the owner 

431 """ 

432 self.assertEqual("NL91ABNA0417164300 - J Test", str(self.no_mandate)) 

433 

434 def test_clean(self) -> None: 

435 """Tests that the model is validated correctly.""" 

436 self.no_mandate.clean() 

437 self.with_mandate.clean() 

438 

439 with self.subTest("Owner required"): 

440 self.no_mandate.owner = None 

441 with self.assertRaises(ValidationError): 

442 self.no_mandate.clean() 

443 

444 self.no_mandate.refresh_from_db() 

445 self.with_mandate.refresh_from_db() 

446 

447 with self.subTest("All mandate fields are non-empty"): 

448 for field in ["valid_from", "signature", "mandate_no"]: 

449 val = getattr(self.with_mandate, field) 

450 setattr(self.with_mandate, field, None) 

451 with self.assertRaises(ValidationError): 

452 self.with_mandate.clean() 

453 setattr(self.with_mandate, field, val) 

454 self.with_mandate.clean() 

455 

456 self.no_mandate.refresh_from_db() 

457 self.with_mandate.refresh_from_db() 

458 

459 with self.subTest("Valid until not before valid from"): 

460 self.with_mandate.valid_until = timezone.now().date() - timezone.timedelta( 

461 days=6 

462 ) 

463 with self.assertRaises(ValidationError): 

464 self.with_mandate.clean() 

465 

466 self.no_mandate.refresh_from_db() 

467 self.with_mandate.refresh_from_db() 

468 

469 with self.subTest("Valid from required to fill valid until"): 

470 self.with_mandate.valid_until = timezone.now().date() 

471 self.with_mandate.clean() 

472 self.with_mandate.valid_from = None 

473 with self.assertRaises(ValidationError): 

474 self.with_mandate.clean() 

475 

476 self.no_mandate.refresh_from_db() 

477 self.with_mandate.refresh_from_db() 

478 

479 with self.subTest("BIC required for non-NL IBANs"): 

480 self.with_mandate.iban = "DE12500105170648489890" 

481 with self.assertRaises(ValidationError): 

482 self.with_mandate.clean() 

483 self.with_mandate.bic = "NBBEBEBB" 

484 self.with_mandate.clean() 

485 

486 

487@freeze_time("2019-01-01") 

488@override_settings(SUSPEND_SIGNALS=True, THALIA_PAY_ENABLED_PAYMENT_METHOD=True) 

489class PaymentUserTest(TestCase): 

490 fixtures = ["members.json"] 

491 

492 @classmethod 

493 def setUpTestData(cls) -> None: 

494 cls.member = PaymentUser.objects.filter(last_name="Wiggers").first() 

495 

496 def test_tpay_enabled(self): 

497 self.assertFalse(self.member.tpay_enabled) 

498 b = BankAccount.objects.create( 

499 owner=self.member, 

500 initials="J", 

501 last_name="Test2", 

502 iban="NL91ABNA0417164300", 

503 mandate_no="11-2", 

504 valid_from=timezone.now().date() - timezone.timedelta(days=5), 

505 last_used=timezone.now().date() - timezone.timedelta(days=5), 

506 signature="base64,png", 

507 ) 

508 self.assertTrue(self.member.tpay_enabled) 

509 

510 b.valid_until = timezone.now().date() - timezone.timedelta(days=1) 

511 b.save() 

512 self.assertFalse(self.member.tpay_enabled) 

513 

514 def test_tpay_balance(self): 

515 payables.payables.register(MockModel, MockPayable) 

516 self.assertEqual(self.member.tpay_balance, 0) 

517 BankAccount.objects.create( 

518 owner=self.member, 

519 initials="J", 

520 last_name="Test2", 

521 iban="NL91ABNA0417164300", 

522 mandate_no="11-2", 

523 valid_from=timezone.now().date() - timezone.timedelta(days=5), 

524 last_used=timezone.now().date() - timezone.timedelta(days=5), 

525 signature="base64,png", 

526 ) 

527 p1 = services.create_payment( 

528 MockPayable(MockModel(self.member)), self.member, Payment.TPAY 

529 ) 

530 self.assertEqual(self.member.tpay_balance, Decimal(-5)) 

531 

532 p2 = services.create_payment( 

533 MockPayable(MockModel(self.member)), self.member, Payment.TPAY 

534 ) 

535 self.assertEqual(self.member.tpay_balance, Decimal(-10)) 

536 

537 batch = Batch.objects.create() 

538 p1.batch = batch 

539 p1.save() 

540 

541 p2.batch = batch 

542 p2.save() 

543 

544 self.assertEqual(self.member.tpay_balance, Decimal(-10)) 

545 

546 batch.processed = True 

547 batch.save() 

548 

549 self.assertEqual(self.member.tpay_balance, 0) 

550 payables.payables._unregister(MockModel) 

551 

552 def test_allow_disallow_tpay(self): 

553 self.assertTrue(self.member.tpay_allowed) 

554 self.member.allow_tpay() 

555 self.assertTrue(self.member.tpay_allowed) 

556 self.member.disallow_tpay() 

557 self.member.refresh_from_db() 

558 self.assertFalse(self.member.tpay_allowed) 

559 

560 

561class BlacklistedPaymentUserTest(TestCase): 

562 fixtures = ["members.json"] 

563 

564 def test_str(self): 

565 member = PaymentUser.objects.filter(last_name="Wiggers").first() 

566 member.disallow_tpay() 

567 self.assertEqual( 

568 str(member.blacklistedpaymentuser), 

569 "Thom Wiggers (thom) (blacklisted from using Thalia Pay)", 

570 )