package order import ( "crypto/rand" "encoding/hex" "encoding/json" "errors" "fmt" "time" "hfb_sys/backend/internal/model" "gorm.io/datatypes" "gorm.io/gorm" "gorm.io/gorm/clause" ) type Repository struct { db *gorm.DB } func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, error) { var createdID uint64 err := r.db.Transaction(func(tx *gorm.DB) error { var listing model.RentalListing if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, req.ListingID).Error; err != nil { return err } if listing.Status != "published" || listing.ReviewStatus != "approved" { return ErrListingUnavailable } if listing.OwnerID == renterID { return ErrCannotRentOwnListing } if req.RentHours < listing.MinRentHours || req.RentHours > listing.MaxRentHours { return ErrInvalidRentHours } var account model.GameAccount if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, listing.AccountID).Error; err != nil { return err } snapshot, err := makeAccountSnapshot(account) if err != nil { return err } orderNo, err := newOrderNo() if err != nil { return err } now := time.Now() rentEnd := now.Add(time.Duration(req.RentHours) * time.Hour) order := model.RentalOrder{ OrderNo: orderNo, ListingID: listing.ID, AccountID: listing.AccountID, OwnerID: listing.OwnerID, RenterID: renterID, RentStartAt: &now, RentEndAt: &rentEnd, RentHours: req.RentHours, RentAmount: listing.PriceHourly * float64(req.RentHours), DepositAmount: listing.DepositAmount, PlatformFee: 0, AccountSnapshot: snapshot, Status: "pending_handoff", HandoffStatus: "pending_owner", SettlementStatus: "unsettled", } if err := tx.Create(&order).Error; err != nil { return err } listing.Status = "rented" account.Status = "rented" if err := tx.Save(&listing).Error; err != nil { return err } if err := tx.Save(&account).Error; err != nil { return err } createdID = order.ID return nil }) if err != nil { return nil, err } return r.FindForUser(renterID, createdID) } func (r *Repository) Cancel(userID uint64, orderID uint64) error { return r.db.Transaction(func(tx *gorm.DB) error { var order model.RentalOrder if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("id = ? AND renter_id = ?", orderID, userID). First(&order).Error; err != nil { return err } if order.Status != "pending_handoff" { return ErrOrderCannotCancel } var listing model.RentalListing if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil { return err } var account model.GameAccount if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil { return err } order.Status = "cancelled" order.HandoffStatus = "cancelled" listing.Status = "published" account.Status = "published" if err := tx.Save(&order).Error; err != nil { return err } if err := tx.Save(&listing).Error; err != nil { return err } return tx.Save(&account).Error }) } func (r *Repository) SubmitHandoff(userID uint64, orderID uint64, req SubmitHandoffRequest) (*HandoffRecordDTO, error) { var recordID uint64 err := r.db.Transaction(func(tx *gorm.DB) error { var order model.RentalOrder if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil { return err } if order.OwnerID != userID { return ErrPermissionDenied } if order.Status != "pending_handoff" || order.HandoffStatus != "pending_owner" { return ErrOrderCannotHandoff } record := model.HandoffRecord{ OrderID: order.ID, FromUserID: order.OwnerID, ToUserID: order.RenterID, Type: "owner_handoff", Content: req.Content, } if err := tx.Create(&record).Error; err != nil { return err } order.HandoffStatus = "pending_renter_confirm" if err := tx.Save(&order).Error; err != nil { return err } recordID = record.ID return nil }) if err != nil { return nil, err } return r.findHandoffRecord(recordID) } func (r *Repository) ConfirmReceive(userID uint64, orderID uint64) error { return r.db.Transaction(func(tx *gorm.DB) error { var order model.RentalOrder if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil { return err } if order.RenterID != userID { return ErrPermissionDenied } if order.Status != "pending_handoff" || order.HandoffStatus != "pending_renter_confirm" { return ErrOrderCannotReceive } now := time.Now() if err := tx.Model(&model.HandoffRecord{}). Where("order_id = ? AND type = ?", order.ID, "owner_handoff"). Update("confirmed_by_renter_at", now).Error; err != nil { return err } order.Status = "renting" order.HandoffStatus = "received" order.RentStartAt = &now rentEnd := now.Add(time.Duration(order.RentHours) * time.Hour) order.RentEndAt = &rentEnd return tx.Save(&order).Error }) } func (r *Repository) HandoffRecords(userID uint64, orderID uint64) ([]HandoffRecordDTO, error) { var order model.RentalOrder if err := r.db.Where("id = ? AND (renter_id = ? OR owner_id = ?)", orderID, userID, userID).First(&order).Error; err != nil { return nil, err } var records []model.HandoffRecord if err := r.db.Where("order_id = ?", orderID).Order("id ASC").Find(&records).Error; err != nil { return nil, err } items := make([]HandoffRecordDTO, 0, len(records)) for _, record := range records { items = append(items, toHandoffDTO(record)) } return items, nil } func (r *Repository) ListForUser(userID uint64) ([]OrderDTO, error) { var rows []orderRow err := r.baseQuery(). Where("o.renter_id = ?", userID). Order("o.id DESC"). Scan(&rows).Error if err != nil { return nil, err } items := make([]OrderDTO, 0, len(rows)) for _, row := range rows { items = append(items, row.toDTO()) } return items, nil } func (r *Repository) FindForUser(userID uint64, orderID uint64) (*OrderDTO, error) { var row orderRow if err := r.baseQuery(). Where("o.id = ? AND (o.renter_id = ? OR o.owner_id = ?)", orderID, userID, userID). First(&row).Error; err != nil { return nil, err } dto := row.toDTO() return &dto, nil } func (r *Repository) findHandoffRecord(id uint64) (*HandoffRecordDTO, error) { var record model.HandoffRecord if err := r.db.First(&record, id).Error; err != nil { return nil, err } dto := toHandoffDTO(record) return &dto, nil } func (r *Repository) baseQuery() *gorm.DB { return r.db.Table("rental_orders AS o"). Select("o.*, a.title, a.server_region, a.login_platform"). Joins("JOIN game_accounts AS a ON a.id = o.account_id") } type orderRow struct { model.RentalOrder Title string ServerRegion string LoginPlatform string } func (row orderRow) toDTO() OrderDTO { return OrderDTO{ ID: row.ID, OrderNo: row.OrderNo, ListingID: row.ListingID, AccountID: row.AccountID, OwnerID: row.OwnerID, RenterID: row.RenterID, Title: row.Title, ServerRegion: row.ServerRegion, LoginPlatform: row.LoginPlatform, RentStartAt: row.RentStartAt, RentEndAt: row.RentEndAt, RentHours: row.RentHours, RentAmount: row.RentAmount, DepositAmount: row.DepositAmount, PlatformFee: row.PlatformFee, AccountSnapshot: row.AccountSnapshot, Status: row.Status, HandoffStatus: row.HandoffStatus, SettlementStatus: row.SettlementStatus, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt, } } func toHandoffDTO(record model.HandoffRecord) HandoffRecordDTO { return HandoffRecordDTO{ ID: record.ID, OrderID: record.OrderID, FromUserID: record.FromUserID, ToUserID: record.ToUserID, Type: record.Type, Content: record.Content, ConfirmedByRenterAt: record.ConfirmedByRenterAt, ConfirmedByOwnerAt: record.ConfirmedByOwnerAt, CreatedAt: record.CreatedAt, } } func makeAccountSnapshot(account model.GameAccount) (datatypes.JSON, error) { payload := map[string]any{ "account_id": account.ID, "title": account.Title, "game_name": account.GameName, "server_region": account.ServerRegion, "login_platform": account.LoginPlatform, "rank_level": account.RankLevel, "haf_coin_amount": account.HafCoinAmount, "asset_summary": account.AssetSummary, "season_tags": account.SeasonTags, "screenshot_urls": account.ScreenshotURLS, "snapshot_version": 1, } raw, err := json.Marshal(payload) return datatypes.JSON(raw), err } func newOrderNo() (string, error) { buf := make([]byte, 4) if _, err := rand.Read(buf); err != nil { return "", err } return fmt.Sprintf("RO%d%s", time.Now().UnixNano(), hex.EncodeToString(buf)), nil } func IsNotFound(err error) bool { return errors.Is(err, gorm.ErrRecordNotFound) }