diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/Charges.kt b/src/main/kotlin/com/android/trisolarisserver/controller/Charges.kt new file mode 100644 index 0000000..547e972 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/Charges.kt @@ -0,0 +1,107 @@ +package com.android.trisolarisserver.controller + +import com.android.trisolarisserver.component.PropertyAccess +import com.android.trisolarisserver.controller.dto.ChargeCreateRequest +import com.android.trisolarisserver.controller.dto.ChargeResponse +import com.android.trisolarisserver.db.repo.BookingRepo +import com.android.trisolarisserver.models.booking.Charge +import com.android.trisolarisserver.models.booking.ChargeType +import com.android.trisolarisserver.models.property.Role +import com.android.trisolarisserver.repo.AppUserRepo +import com.android.trisolarisserver.repo.ChargeRepo +import com.android.trisolarisserver.security.MyPrincipal +import org.springframework.http.HttpStatus +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.server.ResponseStatusException +import java.util.UUID + +@RestController +@RequestMapping("/properties/{propertyId}/bookings/{bookingId}/charges") +class Charges( + private val propertyAccess: PropertyAccess, + private val bookingRepo: BookingRepo, + private val chargeRepo: ChargeRepo, + private val appUserRepo: AppUserRepo +) { + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + @Transactional + fun create( + @PathVariable propertyId: UUID, + @PathVariable bookingId: UUID, + @AuthenticationPrincipal principal: MyPrincipal?, + @RequestBody request: ChargeCreateRequest + ): ChargeResponse { + val actor = requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER, Role.FINANCE) + val booking = bookingRepo.findById(bookingId).orElseThrow { + ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found") + } + if (booking.property.id != propertyId) { + throw ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found for property") + } + if (request.amount <= 0) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "amount must be > 0") + } + val type = parseType(request.type) + val occurredAt = parseOffset(request.occurredAt) ?: nowForProperty(booking.property.timezone) + val createdBy = appUserRepo.findById(actor.userId).orElse(null) + + val charge = Charge( + property = booking.property, + booking = booking, + type = type, + amount = request.amount, + currency = request.currency.trim(), + notes = request.notes?.trim()?.ifBlank { null }, + occurredAt = occurredAt, + createdBy = createdBy + ) + return chargeRepo.save(charge).toResponse() + } + + @GetMapping + fun list( + @PathVariable propertyId: UUID, + @PathVariable bookingId: UUID, + @AuthenticationPrincipal principal: MyPrincipal? + ): List { + requireMember(propertyAccess, propertyId, principal) + val booking = bookingRepo.findById(bookingId).orElseThrow { + ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found") + } + if (booking.property.id != propertyId) { + throw ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found for property") + } + return chargeRepo.findByBookingIdOrderByOccurredAtDesc(bookingId).map { it.toResponse() } + } + + private fun parseType(value: String): ChargeType { + return try { + ChargeType.valueOf(value.trim().uppercase()) + } catch (_: Exception) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown charge type") + } + } +} + +private fun Charge.toResponse(): ChargeResponse { + val id = id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Charge id missing") + return ChargeResponse( + id = id, + bookingId = booking.id!!, + type = type.name, + amount = amount, + currency = currency, + occurredAt = occurredAt, + notes = notes + ) +} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/dto/ChargeDtos.kt b/src/main/kotlin/com/android/trisolarisserver/controller/dto/ChargeDtos.kt new file mode 100644 index 0000000..64fbc5e --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/dto/ChargeDtos.kt @@ -0,0 +1,22 @@ +package com.android.trisolarisserver.controller.dto + +import java.time.OffsetDateTime +import java.util.UUID + +data class ChargeCreateRequest( + val type: String, + val amount: Long, + val currency: String, + val occurredAt: String? = null, + val notes: String? = null +) + +data class ChargeResponse( + val id: UUID, + val bookingId: UUID, + val type: String, + val amount: Long, + val currency: String, + val occurredAt: OffsetDateTime, + val notes: String? +) diff --git a/src/main/kotlin/com/android/trisolarisserver/models/booking/Charge.kt b/src/main/kotlin/com/android/trisolarisserver/models/booking/Charge.kt new file mode 100644 index 0000000..7ed647a --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/models/booking/Charge.kt @@ -0,0 +1,44 @@ +package com.android.trisolarisserver.models.booking + +import com.android.trisolarisserver.models.property.AppUser +import com.android.trisolarisserver.models.property.Property +import jakarta.persistence.* +import java.time.OffsetDateTime +import java.util.UUID + +@Entity +@Table(name = "charge") +class Charge( + @Id + @GeneratedValue + @Column(columnDefinition = "uuid") + val id: UUID? = null, + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "property_id", nullable = false) + var property: Property, + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "booking_id", nullable = false) + var booking: Booking, + + @Enumerated(EnumType.STRING) + @Column(name = "type", nullable = false) + var type: ChargeType, + + @Column(name = "amount", nullable = false) + var amount: Long, + + @Column(name = "currency", nullable = false) + var currency: String, + + @Column(name = "notes") + var notes: String? = null, + + @Column(name = "occurred_at", nullable = false, columnDefinition = "timestamptz") + var occurredAt: OffsetDateTime = OffsetDateTime.now(), + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "created_by") + var createdBy: AppUser? = null +) diff --git a/src/main/kotlin/com/android/trisolarisserver/models/booking/ChargeType.kt b/src/main/kotlin/com/android/trisolarisserver/models/booking/ChargeType.kt new file mode 100644 index 0000000..01b0c8d --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/models/booking/ChargeType.kt @@ -0,0 +1,5 @@ +package com.android.trisolarisserver.models.booking + +enum class ChargeType { + COMMISSION +} diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/ChargeRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/ChargeRepo.kt new file mode 100644 index 0000000..b2b06c2 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/repo/ChargeRepo.kt @@ -0,0 +1,9 @@ +package com.android.trisolarisserver.repo + +import com.android.trisolarisserver.models.booking.Charge +import org.springframework.data.jpa.repository.JpaRepository +import java.util.UUID + +interface ChargeRepo : JpaRepository { + fun findByBookingIdOrderByOccurredAtDesc(bookingId: UUID): List +}