Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/main/kotlin/upbrella/be/config/event/EventConfiguration.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package upbrella.be.config.event

import org.springframework.beans.factory.InitializingBean
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import upbrella.be.util.event.Events

@Configuration
class EventsConfiguration(
private val applicationContext: ApplicationContext
) {

@Bean
fun eventsInitializer(): InitializingBean {
return InitializingBean { Events.setPublisher(applicationContext) }
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,8 @@
package upbrella.be.rent.dto.response

import upbrella.be.rent.entity.ConditionReport

data class ConditionReportResponse(
val id: Long,
val umbrellaUuid: Long,
val content: String?,
val etc: String?
) {
companion object {
fun fromConditionReport(conditionReport: ConditionReport): ConditionReportResponse {
return ConditionReportResponse(
id = conditionReport.history.id!!,
umbrellaUuid = conditionReport.history.umbrella.uuid,
content = conditionReport.content,
etc = conditionReport.etc
)
}
}
}
)
Original file line number Diff line number Diff line change
@@ -1,21 +1,8 @@
package upbrella.be.rent.dto.response

import upbrella.be.rent.entity.ImprovementReport

data class ImprovementReportResponse(
val id: Long = 0,
val umbrellaUuid: Long = 0,
val content: String? = "",
val etc: String? = ""
) {
companion object {
fun fromImprovementReport(improvementReport: ImprovementReport): ImprovementReportResponse {
return ImprovementReportResponse(
id = improvementReport.history.id ?: 0,
umbrellaUuid = improvementReport.history.umbrella.uuid,
content = improvementReport.content,
etc = improvementReport.etc
)
}
}
}
)
4 changes: 1 addition & 3 deletions src/main/kotlin/upbrella/be/rent/entity/ConditionReport.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import javax.persistence.*

@Entity
class ConditionReport(
@OneToOne
@JoinColumn(name = "history_id")
val history: History,
val historyId: Long,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Explicitly map historyId to the DB column to avoid naming strategy surprises

Previously the FK column was history_id via @JoinColumn. Now that it's a scalar, relying on implicit naming may break if the physical naming strategy changes. Map it explicitly.

Apply this diff:

 class ConditionReport(
-    val historyId: Long,
+    @Column(name = "history_id", nullable = false)
+    val historyId: Long,
     val content: String? = null,
     val etc: String? = null,
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
     val id: Long? = null
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val historyId: Long,
class ConditionReport(
@Column(name = "history_id", nullable = false)
val historyId: Long,
val content: String? = null,
val etc: String? = null,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null
)
🤖 Prompt for AI Agents
In src/main/kotlin/upbrella/be/rent/entity/ConditionReport.kt around line 7, the
scalar property historyId is not explicitly mapped to the DB column and can
break if the physical naming strategy changes; add an explicit JPA column
mapping by annotating the property with @Column(name = "history_id") (and import
javax.persistence.Column if not present) so the field is reliably persisted to
the expected history_id column.

val content: String? = null,
val etc: String? = null,
@Id
Expand Down
4 changes: 1 addition & 3 deletions src/main/kotlin/upbrella/be/rent/entity/ImprovementReport.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import javax.persistence.*

@Entity
class ImprovementReport(
@OneToOne
@JoinColumn(name = "history_id")
val history: History,
val historyId: Long,
val content: String? = null,
val etc: String? = null,
@Id
Expand Down
88 changes: 88 additions & 0 deletions src/main/kotlin/upbrella/be/rent/event/RentEventHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package upbrella.be.rent.event

import org.springframework.context.event.EventListener
import org.springframework.stereotype.Service
import upbrella.be.rent.entity.ConditionReport
import upbrella.be.rent.entity.ImprovementReport
import upbrella.be.rent.service.ConditionReportService
import upbrella.be.rent.service.ImprovementReportService
import upbrella.be.rent.service.RentService
import upbrella.be.slack.SlackAlarmService
import upbrella.be.slack.dto.service.input.NotifyConditionReportInput
import upbrella.be.slack.dto.service.input.NotifyImprovementReportInput
import upbrella.be.slack.dto.service.input.NotifyRentInput
import upbrella.be.slack.dto.service.input.NotifyReturnInput

@Service
class RentEventHandler(
private val slackAlarmService: SlackAlarmService,
private val conditionReportService: ConditionReportService,
private val rentService: RentService,
private val improvementReportService: ImprovementReportService,
) {

@EventListener(UmbrellaRentedEvent::class)
fun handleUmbrellaRentedEvent(event: UmbrellaRentedEvent) {
slackAlarmService.notifyRent(
NotifyRentInput(
userId = event.userId,
userName = event.userName,
rentStoreName = event.rentStoreName
)
)

event.conditionReportContent
?.takeIf { it.isNotBlank() }
?.let { content ->
ConditionReport(
historyId = event.historyId,
content = content
).also { conditionReport ->
conditionReportService.saveConditionReport(conditionReport)
slackAlarmService.notifyConditionReport(
NotifyConditionReportInput(
umbrellaId = event.umbrellaId,
rentStoreName = event.rentStoreName,
content = content
)
)
}
}
}

@EventListener(UmbrellaReturnedEvent::class)
fun handleUmbrellaReturnedEvent(event: UmbrellaReturnedEvent) {

val unrefundedRentCount = rentService.countUnrefundedRent()
slackAlarmService.notifyReturn(
NotifyReturnInput(
userId = event.rentUserId,
rentStoreName = event.rentStoreName,
rentedAt = event.rentedAt.toString(),
returnStoreName = event.returnStoreName,
returnedAt = event.returnedAt.toString(),
unrefundedCount = unrefundedRentCount
)
)

event.improvementReportContent?.takeIf { it.isNotBlank() }
?.let { content ->
ImprovementReport(
historyId = event.historyId,
content = content
).also { improvementReport ->
improvementReportService.save(improvementReport)
slackAlarmService.notifyImprovementReport(
NotifyImprovementReportInput(
umbrellaId = event.returnedUmbrellaId,
rentStoreName = event.rentStoreName,
rentedAt = event.rentedAt.toString(),
returnStoreName = event.returnStoreName,
returnedAt = event.returnedAt.toString(),
content = content
)
)
}
}
}
Comment on lines +53 to +87
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Apply the same transaction phase separation for return events.

Similar to the rental event handler, separate persistence from notifications to ensure Slack failures don't impact the return transaction.

-    @EventListener(UmbrellaReturnedEvent::class)
-    fun handleUmbrellaReturnedEvent(event: UmbrellaReturnedEvent) {
+    // Persist improvement reports within the transaction
+    @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
+    fun persistImprovementReport(event: UmbrellaReturnedEvent) {
+        event.improvementReportContent?.takeIf { it.isNotBlank() }
+            ?.let { content ->
+                val improvementReport = ImprovementReport(
+                    historyId = event.historyId,
+                    content = content
+                )
+                improvementReportService.save(improvementReport)
+            }
+    }
+
+    // Send notifications after successful commit
+    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
+    fun notifyReturnAfterCommit(event: UmbrellaReturnedEvent) {
         val unrefundedRentCount = rentService.countUnrefundedRent()
         slackAlarmService.notifyReturn(
             NotifyReturnInput(
                 userId = event.rentUserId,
                 rentStoreName = event.rentStoreName,
                 rentedAt = event.rentedAt.toString(),
                 returnStoreName = event.returnStoreName,
                 returnedAt = event.returnedAt.toString(),
                 unrefundedCount = unrefundedRentCount
             )
         )
 
         event.improvementReportContent?.takeIf { it.isNotBlank() }
             ?.let { content ->
-                ImprovementReport(
-                    historyId = event.historyId,
-                    content = content
-                ).also { improvementReport ->
-                    improvementReportService.save(improvementReport)
-                    slackAlarmService.notifyImprovementReport(
-                        NotifyImprovementReportInput(
-                            umbrellaId = event.returnedUmbrellaId,
-                            rentStoreName = event.rentStoreName,
-                            rentedAt = event.rentedAt.toString(),
-                            returnStoreName = event.returnStoreName,
-                            returnedAt = event.returnedAt.toString(),
-                            content = content
-                        )
+                slackAlarmService.notifyImprovementReport(
+                    NotifyImprovementReportInput(
+                        umbrellaId = event.returnedUmbrellaId,
+                        rentStoreName = event.rentStoreName,
+                        rentedAt = event.rentedAt.toString(),
+                        returnStoreName = event.returnStoreName,
+                        returnedAt = event.returnedAt.toString(),
+                        content = content
                     )
-                }
+                )
             }
     }
🤖 Prompt for AI Agents
In src/main/kotlin/upbrella/be/rent/event/RentEventHandler.kt around lines
53–87, the handler currently mixes persistence (saving improvement reports /
counting unrefunded rents) with Slack notification calls so Slack failures can
break the return transaction; separate persistence from notifications by
ensuring all DB actions run inside the transaction and notifications run only
after commit: either (A) split the logic into two methods — keep the DB work
(countUnrefundedRent, improvementReportService.save) in the transactional
listener, and move slackAlarmService.notifyReturn / notifyImprovementReport into
a separate listener annotated with @TransactionalEventListener(phase =
AFTER_COMMIT), or (B) persist the improvement report and compute unrefunded
count first, then trigger notifications asynchronously after the transaction
completes; implement one of these approaches so Slack calls cannot roll back or
fail the DB transaction.

}
12 changes: 12 additions & 0 deletions src/main/kotlin/upbrella/be/rent/event/UmbrellaRentedEvent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package upbrella.be.rent.event

import upbrella.be.util.event.Event

class UmbrellaRentedEvent(
val userId: Long,
val userName: String,
val rentStoreName: String,
val conditionReportContent: String? = null,
val umbrellaId: Long,
val historyId: Long,
) : Event()
15 changes: 15 additions & 0 deletions src/main/kotlin/upbrella/be/rent/event/UmbrellaReturnedEvent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package upbrella.be.rent.event

import upbrella.be.util.event.Event
import java.time.LocalDateTime

class UmbrellaReturnedEvent(
val rentUserId: Long,
val rentStoreName: String,
val rentedAt: LocalDateTime,
val returnStoreName: String,
val returnedAt: LocalDateTime,
val historyId: Long,
val improvementReportContent: String? = null,
val returnedUmbrellaId: Long,
) : Event()
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package upbrella.be.rent.repository

import com.querydsl.core.types.Projections
import com.querydsl.jpa.impl.JPAQueryFactory
import org.springframework.stereotype.Repository
import upbrella.be.rent.dto.response.ConditionReportResponse
import upbrella.be.rent.entity.QConditionReport.conditionReport
import upbrella.be.rent.entity.QHistory.history

@Repository
class CustomConditionReportRepository(
private val queryFactory: JPAQueryFactory
) {
fun findAllConditionReport(): List<ConditionReportResponse> {
return queryFactory
.select(
Projections.constructor(
ConditionReportResponse::class.java,
conditionReport.id,
history.umbrella.uuid,
conditionReport.content,
conditionReport.etc
)
)
.from(conditionReport)
.join(history)
.fetch()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package upbrella.be.rent.repository

import com.querydsl.core.types.Projections
import com.querydsl.jpa.impl.JPAQueryFactory
import org.springframework.stereotype.Repository
import upbrella.be.rent.dto.response.ImprovementReportResponse
import upbrella.be.rent.entity.QHistory.history
import upbrella.be.rent.entity.QImprovementReport.improvementReport

@Repository
class CustomImprovementReportRepository(
private val queryFactory: JPAQueryFactory,
) {
fun findAllImprovementReport(): List<ImprovementReportResponse> {
return queryFactory
.select(
Projections.constructor(
ImprovementReportResponse::class.java,
improvementReport.id,
history.umbrella.uuid,
improvementReport.content,
improvementReport.etc
)
)
.from(improvementReport)
.join(history)
.fetchJoin()
.fetch()
Comment on lines +25 to +28
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Bug: Cartesian join and invalid fetchJoin due to missing association

.join(history).fetchJoin() without an ON clause produces a Cartesian product, because ImprovementReport no longer has an association to History. Also, fetchJoin is only valid for association path joins. This will return incorrect/duplicated rows and may throw at runtime depending on the JPA provider.

Fix the join by joining on the FK and remove fetchJoin:

             .from(improvementReport)
-            .join(history)
-            .fetchJoin()
+            .join(history)
+            .on(history.id.eq(improvementReport.historyId))
             .fetch()

If some ImprovementReports could reference missing History rows (ideally they should not), use leftJoin instead of join.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.from(improvementReport)
.join(history)
.fetchJoin()
.fetch()
//
.from(improvementReport)
.join(history)
.on(history.id.eq(improvementReport.historyId))
.fetch()
🤖 Prompt for AI Agents
In
src/main/kotlin/upbrella/be/rent/repository/CustomImprovementReportRepository.kt
around lines 25 to 28, the code uses .join(history).fetchJoin() which creates a
Cartesian product and is invalid because ImprovementReport no longer has an
association to History; change the join to an explicit join-on using the FK
(e.g. join(history).on(improvementReport.historyId.eq(history.id))) and remove
fetchJoin() since fetchJoin only applies to path associations; if some
ImprovementReport rows may lack a matching History use leftJoin(...).on(...)
instead of join(...).

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,20 @@ package upbrella.be.rent.service

import org.springframework.stereotype.Service
import upbrella.be.rent.dto.response.ConditionReportPageResponse
import upbrella.be.rent.dto.response.ConditionReportResponse
import upbrella.be.rent.entity.ConditionReport
import upbrella.be.rent.repository.ConditionReportRepository
import upbrella.be.rent.repository.CustomConditionReportRepository

@Service
class ConditionReportService(
private val conditionReportRepository: ConditionReportRepository
private val conditionReportRepository: ConditionReportRepository,
private val customConditionReportRepository: CustomConditionReportRepository,
) {

fun findAll(): ConditionReportPageResponse =
ConditionReportPageResponse.of(findAllConditionReport())
ConditionReportPageResponse.of(customConditionReportRepository.findAllConditionReport())

fun saveConditionReport(conditionReport: ConditionReport) {
conditionReportRepository.save(conditionReport)
}

private fun findAllConditionReport(): List<ConditionReportResponse> =
conditionReportRepository.findAll()
.map { ConditionReportResponse.fromConditionReport(it) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,21 @@ package upbrella.be.rent.service

import org.springframework.stereotype.Service
import upbrella.be.rent.dto.response.ImprovementReportPageResponse
import upbrella.be.rent.dto.response.ImprovementReportResponse
import upbrella.be.rent.entity.ImprovementReport
import upbrella.be.rent.repository.CustomImprovementReportRepository
import upbrella.be.rent.repository.ImprovementReportRepository

@Service
class ImprovementReportService(
private val improvementReportRepository: ImprovementReportRepository
private val improvementReportRepository: ImprovementReportRepository,
private val customImprovementReportRepository: CustomImprovementReportRepository
) {

fun findAll(): ImprovementReportPageResponse =
ImprovementReportPageResponse.of(findAllImprovementReport())
ImprovementReportPageResponse.of(customImprovementReportRepository.findAllImprovementReport())

fun save(improvementReport: ImprovementReport) {

improvementReportRepository.save(improvementReport)
}

private fun findAllImprovementReport(): List<ImprovementReportResponse> =
improvementReportRepository.findAll()
.map { ImprovementReportResponse.fromImprovementReport(it) }
}
Loading