From 2f0ec8bf5b191b2a68ff679643735faf8def6ccf Mon Sep 17 00:00:00 2001 From: Petri Lehtinen Date: Thu, 20 Feb 2025 09:04:19 +0200 Subject: [PATCH] Don't replace zero invoices with a new zero invoice This could happen if new invoices were not generated for the head of family anymore because the placement ended, for example. --- .../ReplacementInvoicesIntegrationTest.kt | 122 ++++++++++++++---- .../invoicing/service/InvoiceGenerator.kt | 4 +- 2 files changed, 99 insertions(+), 27 deletions(-) diff --git a/service/src/integrationTest/kotlin/fi/espoo/evaka/invoicing/service/ReplacementInvoicesIntegrationTest.kt b/service/src/integrationTest/kotlin/fi/espoo/evaka/invoicing/service/ReplacementInvoicesIntegrationTest.kt index 72433359118..6de8ea0bcc4 100644 --- a/service/src/integrationTest/kotlin/fi/espoo/evaka/invoicing/service/ReplacementInvoicesIntegrationTest.kt +++ b/service/src/integrationTest/kotlin/fi/espoo/evaka/invoicing/service/ReplacementInvoicesIntegrationTest.kt @@ -14,7 +14,9 @@ import fi.espoo.evaka.invoicing.domain.FeeDecisionStatus import fi.espoo.evaka.invoicing.domain.InvoiceDetailed import fi.espoo.evaka.invoicing.domain.InvoiceReplacementReason import fi.espoo.evaka.invoicing.domain.InvoiceStatus +import fi.espoo.evaka.shared.FeeDecisionId import fi.espoo.evaka.shared.PersonId +import fi.espoo.evaka.shared.PlacementId import fi.espoo.evaka.shared.auth.AuthenticatedUser import fi.espoo.evaka.shared.auth.UserRole import fi.espoo.evaka.shared.dev.DevAbsence @@ -376,6 +378,75 @@ class ReplacementInvoicesIntegrationTest : FullApplicationTest(resetDbBeforeEach assertEquals(replacementAfter.status, InvoiceStatus.SENT) } + @Test + fun `no replacement drafts are created anymore after placement ends`() { + val employee = DevEmployee(roles = setOf(UserRole.FINANCE_ADMIN)) + db.transaction { tx -> tx.insert(employee) } + + val month1 = previousMonth.minusMonths(1) + val month2 = previousMonth + + val (placementId, feeDecisionId) = + insertPlacementAndFeeDecision( + range = + // 2 months + FiniteDateRange(month1.atDay(1), month2.atEndOfMonth()) + ) + + generateAndSendInvoices(month1) + generateAndSendInvoices(month2) + val original = + db.read { it.searchInvoices(period = FiniteDateRange.ofMonth(month2)) }.single() + + // Shorten the placement and fee decision to only cover month1 => a zero replacement is + // created for month2 + db.transaction { tx -> + tx.execute { + sql( + """ + UPDATE placement + SET end_date = ${bind(month1.atEndOfMonth())} + WHERE id = ${bind(placementId)} + """ + ) + } + tx.execute { + sql( + """ + UPDATE fee_decision + SET valid_during = ${bind(FiniteDateRange.ofMonth(month1))} + WHERE id = ${bind(feeDecisionId)} + """ + ) + } + } + val replacement = generateReplacementDrafts().single() + assertEquals(month2, YearMonth.from(replacement.periodStart)) + assertEquals(original.id, replacement.replacedInvoiceId) + assertEquals(1, replacement.revisionNumber) + assertEquals(0, replacement.totalPrice) + + invoiceController.markReplacementDraftSent( + dbInstance(), + employee.user, + MockEvakaClock(now), + replacement.id, + InvoiceController.MarkReplacementDraftSentRequest( + reason = InvoiceReplacementReason.OTHER, + notes = "ended early", + ), + ) + + val originalAfter = db.read { tx -> tx.getInvoice(original.id) }!! + assertEquals(InvoiceStatus.REPLACED, originalAfter.status) + val replacementAfter = db.read { tx -> tx.getInvoice(replacement.id) }!! + assertEquals(InvoiceStatus.SENT, replacementAfter.status) + + // A new replacement draft is not created anymore + val replacements = generateReplacementDrafts() + assertEquals(emptyList(), replacements) + } + private fun generateReplacementDrafts(today: LocalDate = this.today): List { invoiceGenerator.generateAllReplacementDraftInvoices(db, today) return db.read { tx -> tx.searchInvoices(InvoiceStatus.REPLACEMENT_DRAFT) } @@ -393,38 +464,37 @@ class ReplacementInvoicesIntegrationTest : FullApplicationTest(resetDbBeforeEach child: DevPerson = this.child, fee: Int = 29500, range: FiniteDateRange = FiniteDateRange.ofMonth(previousMonth), - ) { + ): Pair { + val placement = + DevPlacement( + childId = child.id, + unitId = daycare.id, + startDate = range.start, + endDate = range.end, + ) + val feeDecision = + DevFeeDecision( + status = FeeDecisionStatus.SENT, + validDuring = range, + headOfFamilyId = headOfFamily.id, + totalFee = 29500, + ) db.transaction { tx -> + tx.insert(placement) + tx.insert(feeDecision) tx.insert( - DevPlacement( + DevFeeDecisionChild( + feeDecisionId = feeDecision.id, childId = child.id, - unitId = daycare.id, - startDate = range.start, - endDate = range.end, + childDateOfBirth = child.dateOfBirth, + placementUnitId = daycare.id, + baseFee = fee, + fee = fee, + finalFee = fee, ) ) - tx.insert( - DevFeeDecision( - status = FeeDecisionStatus.SENT, - validDuring = range, - headOfFamilyId = headOfFamily.id, - totalFee = 29500, - ) - ) - .also { feeDecisionId -> - tx.insert( - DevFeeDecisionChild( - feeDecisionId = feeDecisionId, - childId = child.id, - childDateOfBirth = child.dateOfBirth, - placementUnitId = daycare.id, - baseFee = fee, - fee = fee, - finalFee = fee, - ) - ) - } } + return placement.id to feeDecision.id } private fun generateAndSendInvoices(month: YearMonth = previousMonth): List = diff --git a/service/src/main/kotlin/fi/espoo/evaka/invoicing/service/InvoiceGenerator.kt b/service/src/main/kotlin/fi/espoo/evaka/invoicing/service/InvoiceGenerator.kt index f8998f35791..0dce3305d8d 100755 --- a/service/src/main/kotlin/fi/espoo/evaka/invoicing/service/InvoiceGenerator.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/invoicing/service/InvoiceGenerator.kt @@ -183,9 +183,11 @@ class InvoiceGenerator( } } val zeroInvoices = - // Sent invoices that don't have a corresponding draft invoice -> add a zero-priced + // Sent non-zero invoices that don't have a corresponding draft invoice -> add a + // zero-priced // replacement draft sentInvoices.values + .filterNot { it.totalPrice == 0 } .filterNot { headsOfFamilyWithInvoices.contains(it.headOfFamily.id) } .map { sentInvoice -> DraftInvoice(