This commit is contained in:
2026-01-25 21:59:00 +08:00
parent 7fd537bef3
commit 4cad3f0250
118 changed files with 30473 additions and 0 deletions

View File

@@ -0,0 +1,182 @@
package handler
import (
"errors"
"strconv"
"accounting-app/pkg/api"
"accounting-app/internal/service"
"github.com/gin-gonic/gin"
)
// CreditAccountHandler handles HTTP requests for credit account operations
type CreditAccountHandler struct {
billingService *service.BillingService
repaymentService *service.RepaymentService
}
// NewCreditAccountHandler creates a new CreditAccountHandler instance
func NewCreditAccountHandler(
billingService *service.BillingService,
repaymentService *service.RepaymentService,
) *CreditAccountHandler {
return &CreditAccountHandler{
billingService: billingService,
repaymentService: repaymentService,
}
}
// GetBills handles GET /api/v1/accounts/:id/bills
// Returns all bills for a specific credit account
func (h *CreditAccountHandler) GetBills(c *gin.Context) {
userId, exists := c.Get("user_id")
if !exists {
api.Unauthorized(c, "User not authenticated")
return
}
accountID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
api.BadRequest(c, "Invalid account ID")
return
}
bills, err := h.billingService.GetBillsByAccountID(userId.(uint), uint(accountID))
if err != nil {
api.InternalError(c, "Failed to get bills: "+err.Error())
return
}
api.Success(c, bills)
}
// RepayInput represents the input for repayment
type RepayInput struct {
BillID uint `json:"bill_id" binding:"required"`
Amount float64 `json:"amount" binding:"required,gt=0"`
FromAccountID uint `json:"from_account_id" binding:"required"`
}
// Repay handles POST /api/v1/accounts/:id/repay
// Processes a repayment for a credit account bill
func (h *CreditAccountHandler) Repay(c *gin.Context) {
userId, exists := c.Get("user_id")
if !exists {
api.Unauthorized(c, "User not authenticated")
return
}
accountID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
api.BadRequest(c, "Invalid account ID")
return
}
var input RepayInput
if err := c.ShouldBindJSON(&input); err != nil {
api.ValidationError(c, "Invalid request body: "+err.Error())
return
}
// Get the bill to verify it belongs to this account
bill, err := h.billingService.GetBillByID(userId.(uint), input.BillID)
if err != nil {
if errors.Is(err, service.ErrBillNotFound) {
api.NotFound(c, "Bill not found")
return
}
api.InternalError(c, "Failed to get bill: "+err.Error())
return
}
// Verify bill belongs to the specified account
if bill.AccountID != uint(accountID) {
api.BadRequest(c, "Bill does not belong to the specified account")
return
}
// Check if bill is already paid
if bill.Status == "paid" {
api.BadRequest(c, "Bill is already paid")
return
}
// Check if payment amount exceeds bill balance
if input.Amount > bill.CurrentBalance {
api.BadRequest(c, "Payment amount exceeds bill balance")
return
}
// Check if bill has a repayment plan
plan, err := h.repaymentService.GetRepaymentPlanByBillID(userId.(uint), input.BillID)
if err != nil && !errors.Is(err, service.ErrRepaymentPlanNotFound) {
api.InternalError(c, "Failed to check repayment plan: "+err.Error())
return
}
// If bill has a repayment plan, use installment payment
if plan != nil {
// Find the next unpaid installment
var nextInstallment *uint
for _, installment := range plan.Installments {
if installment.Status != "paid" {
nextInstallment = &installment.ID
break
}
}
if nextInstallment == nil {
api.BadRequest(c, "All installments are already paid")
return
}
// Pay the installment
payInput := service.PayInstallmentInput{
InstallmentID: *nextInstallment,
Amount: input.Amount,
FromAccountID: input.FromAccountID,
}
if err := h.repaymentService.PayInstallment(userId.(uint), payInput); err != nil {
if errors.Is(err, service.ErrInvalidRepaymentAmount) {
api.BadRequest(c, "Invalid payment amount")
return
}
if errors.Is(err, service.ErrPaymentExceedsInstallment) {
api.BadRequest(c, "Payment amount exceeds installment amount")
return
}
if errors.Is(err, service.ErrInstallmentAlreadyPaid) {
api.BadRequest(c, "Installment is already paid")
return
}
api.InternalError(c, "Failed to process payment: "+err.Error())
return
}
api.Success(c, gin.H{
"message": "Payment processed successfully via repayment plan",
"plan_id": plan.ID,
})
return
}
// If no repayment plan, process as direct payment
// This would require a direct payment method in the billing service
// For now, we'll suggest creating a repayment plan for structured payments
api.Success(c, gin.H{
"message": "Direct payment not yet implemented. Please create a repayment plan first.",
"suggestion": "Create a repayment plan with POST /api/v1/repayment-plans",
})
}
// RegisterRoutes registers credit account routes to the given router group
func (h *CreditAccountHandler) RegisterRoutes(rg *gin.RouterGroup) {
// Credit account specific routes are nested under accounts
accounts := rg.Group("/accounts")
{
accounts.GET("/:id/bills", h.GetBills)
accounts.POST("/:id/repay", h.Repay)
}
}