修复取消订单误释放商品
This commit is contained in:
@@ -230,6 +230,19 @@ func clampRenterRetentionDays(value int) int {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasActiveSiblingOrder(tx *gorm.DB, order model.RentalOrder) (bool, error) {
|
||||||
|
var count int64
|
||||||
|
err := tx.Model(&model.RentalOrder{}).
|
||||||
|
Where("id <> ? AND (listing_id = ? OR account_id = ?) AND status NOT IN ?",
|
||||||
|
order.ID,
|
||||||
|
order.ListingID,
|
||||||
|
order.AccountID,
|
||||||
|
[]string{"completed", "cancelled", "closed"},
|
||||||
|
).
|
||||||
|
Count(&count).Error
|
||||||
|
return count > 0, err
|
||||||
|
}
|
||||||
|
|
||||||
func (j *Job) handlePendingPaymentTimeout(ctx context.Context, now time.Time, cfg thresholds) (int, error) {
|
func (j *Job) handlePendingPaymentTimeout(ctx context.Context, now time.Time, cfg thresholds) (int, error) {
|
||||||
if cfg.PendingPaymentTimeoutMinutes <= 0 {
|
if cfg.PendingPaymentTimeoutMinutes <= 0 {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
@@ -260,7 +273,13 @@ func (j *Job) handlePendingPaymentTimeout(ctx context.Context, now time.Time, cf
|
|||||||
}
|
}
|
||||||
order.Status = "cancelled"
|
order.Status = "cancelled"
|
||||||
order.HandoffStatus = "cancelled"
|
order.HandoffStatus = "cancelled"
|
||||||
listing.InTransaction = false
|
active, err := hasActiveSiblingOrder(tx, order)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !active {
|
||||||
|
listing.InTransaction = false
|
||||||
|
}
|
||||||
orderID := order.ID
|
orderID := order.ID
|
||||||
if err := closePendingOrderPayments(tx, order.ID, "order_timeout"); err != nil {
|
if err := closePendingOrderPayments(tx, order.ID, "order_timeout"); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -447,6 +447,13 @@ func (r *Repository) AdminRejectRefund(ctx context.Context, orderID uint64, acti
|
|||||||
if order.Status != orderStatusCancelled {
|
if order.Status != orderStatusCancelled {
|
||||||
return errors.New("仅已取消的订单可以恢复")
|
return errors.New("仅已取消的订单可以恢复")
|
||||||
}
|
}
|
||||||
|
active, err := hasActiveSiblingOrder(tx, order)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if active {
|
||||||
|
return ErrListingUnavailable
|
||||||
|
}
|
||||||
order.Status = orderStatusPendingHandoff
|
order.Status = orderStatusPendingHandoff
|
||||||
if order.HandoffStatus == handoffStatusCancelled || order.HandoffStatus == handoffStatusRenterConfirmTimeout {
|
if order.HandoffStatus == handoffStatusCancelled || order.HandoffStatus == handoffStatusRenterConfirmTimeout {
|
||||||
order.HandoffStatus = handoffStatusPendingOwner
|
order.HandoffStatus = handoffStatusPendingOwner
|
||||||
|
|||||||
@@ -61,6 +61,50 @@ func releaseAssetsForRental(tx *gorm.DB, listing *model.RentalListing, account *
|
|||||||
return listingstatus.AppendTransition(tx, listing, from, listingstatus.SourceOrder, listingstatus.ActorSystem, 0, "订单取消恢复上架")
|
return listingstatus.AppendTransition(tx, listing, from, listingstatus.SourceOrder, listingstatus.ActorSystem, 0, "订单取消恢复上架")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func terminalOrderStatuses() []string {
|
||||||
|
return []string{orderStatusCompleted, orderStatusCancelled, orderStatusClosed}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasActiveSiblingOrder(tx *gorm.DB, order *model.RentalOrder) (bool, error) {
|
||||||
|
if order == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
var count int64
|
||||||
|
err := tx.Model(&model.RentalOrder{}).
|
||||||
|
Where("id <> ? AND (listing_id = ? OR account_id = ?) AND status NOT IN ?",
|
||||||
|
order.ID,
|
||||||
|
order.ListingID,
|
||||||
|
order.AccountID,
|
||||||
|
terminalOrderStatuses(),
|
||||||
|
).
|
||||||
|
Count(&count).Error
|
||||||
|
return count > 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasActiveOrderForAssets(tx *gorm.DB, listingID uint64, accountID uint64, excludeOrderID uint64) (bool, error) {
|
||||||
|
var count int64
|
||||||
|
err := tx.Model(&model.RentalOrder{}).
|
||||||
|
Where("id <> ? AND (listing_id = ? OR account_id = ?) AND status NOT IN ?",
|
||||||
|
excludeOrderID,
|
||||||
|
listingID,
|
||||||
|
accountID,
|
||||||
|
terminalOrderStatuses(),
|
||||||
|
).
|
||||||
|
Count(&count).Error
|
||||||
|
return count > 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func releaseAssetsForRentalIfIdle(tx *gorm.DB, order *model.RentalOrder, listing *model.RentalListing, account *model.GameAccount) error {
|
||||||
|
active, err := hasActiveSiblingOrder(tx, order)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if active {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return releaseAssetsForRental(tx, listing, account)
|
||||||
|
}
|
||||||
|
|
||||||
func archiveAssets(tx *gorm.DB, listing *model.RentalListing, account *model.GameAccount) error {
|
func archiveAssets(tx *gorm.DB, listing *model.RentalListing, account *model.GameAccount) error {
|
||||||
from := listing.Status
|
from := listing.Status
|
||||||
listing.Status = listingStatusOffline
|
listing.Status = listingStatusOffline
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ func (r *Repository) Create(ctx context.Context, renterID uint64, req CreateRequ
|
|||||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, listing.AccountID).Error; err != nil {
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, listing.AccountID).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
active, err := hasActiveOrderForAssets(tx, listing.ID, account.ID, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if active {
|
||||||
|
return ErrListingUnavailable
|
||||||
|
}
|
||||||
snapshot, err := makeAccountSnapshot(account, listing)
|
snapshot, err := makeAccountSnapshot(account, listing)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -219,6 +226,13 @@ func (r *Repository) ConfirmPaidFromChannelTx(tx *gorm.DB, orderID uint64) (uint
|
|||||||
if listing.Status != listingStatusPublished || listing.ReviewStatus != listingReviewStatusApproved || !listing.InTransaction {
|
if listing.Status != listingStatusPublished || listing.ReviewStatus != listingReviewStatusApproved || !listing.InTransaction {
|
||||||
return 0, ErrListingUnavailable
|
return 0, ErrListingUnavailable
|
||||||
}
|
}
|
||||||
|
active, err := hasActiveSiblingOrder(tx, order)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if active {
|
||||||
|
return 0, ErrListingUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
// 租客已通过外部渠道付款,这里不写租客钱包流水。
|
// 租客已通过外部渠道付款,这里不写租客钱包流水。
|
||||||
orderID = order.ID
|
orderID = order.ID
|
||||||
@@ -354,7 +368,7 @@ func (r *Repository) Cancel(ctx context.Context, userID uint64, orderID uint64)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := releaseAssetsForRental(tx, listing, account); err != nil {
|
if err := releaseAssetsForRentalIfIdle(tx, &order, listing, account); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := closePendingOrderPayments(tx, order.ID, "order_cancel"); err != nil {
|
if err := closePendingOrderPayments(tx, order.ID, "order_cancel"); err != nil {
|
||||||
|
|||||||
@@ -140,6 +140,63 @@ func TestRepositoryCreateOrderRejectsOwnListing(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRepositoryCreateOrderRejectsActiveSiblingOrder(t *testing.T) {
|
||||||
|
db := setupOrderTestDB(t)
|
||||||
|
repo := NewRepository(db)
|
||||||
|
|
||||||
|
owner := model.User{Phone: "13800000011"}
|
||||||
|
renter := model.User{Phone: "13800000012"}
|
||||||
|
activeRenter := model.User{Phone: "13800000013"}
|
||||||
|
if err := db.Create(&owner).Error; err != nil {
|
||||||
|
t.Fatalf("create owner failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&renter).Error; err != nil {
|
||||||
|
t.Fatalf("create renter failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&activeRenter).Error; err != nil {
|
||||||
|
t.Fatalf("create active renter failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
account := model.GameAccount{
|
||||||
|
OwnerID: owner.ID,
|
||||||
|
ServerRegion: "国服",
|
||||||
|
LoginPlatform: "steam",
|
||||||
|
Title: "测试账号",
|
||||||
|
Status: accountStatusPublished,
|
||||||
|
}
|
||||||
|
if err := db.Create(&account).Error; err != nil {
|
||||||
|
t.Fatalf("create account failed: %v", err)
|
||||||
|
}
|
||||||
|
listing := model.RentalListing{
|
||||||
|
AccountID: account.ID,
|
||||||
|
OwnerID: owner.ID,
|
||||||
|
PriceCent: 1000,
|
||||||
|
Status: listingStatusPublished,
|
||||||
|
ReviewStatus: listingReviewStatusApproved,
|
||||||
|
InTransaction: false,
|
||||||
|
}
|
||||||
|
if err := db.Create(&listing).Error; err != nil {
|
||||||
|
t.Fatalf("create listing failed: %v", err)
|
||||||
|
}
|
||||||
|
activeOrder := model.RentalOrder{
|
||||||
|
OrderNo: "ORD-ACTIVE-SIBLING-001",
|
||||||
|
ListingID: listing.ID,
|
||||||
|
AccountID: account.ID,
|
||||||
|
OwnerID: owner.ID,
|
||||||
|
RenterID: activeRenter.ID,
|
||||||
|
Status: orderStatusRenting,
|
||||||
|
HandoffStatus: handoffStatusReceived,
|
||||||
|
}
|
||||||
|
if err := db.Create(&activeOrder).Error; err != nil {
|
||||||
|
t.Fatalf("create active order failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := repo.Create(t.Context(), renter.ID, CreateRequest{ListingID: listing.ID})
|
||||||
|
if err != ErrListingUnavailable {
|
||||||
|
t.Fatalf("Create() error = %v, want ErrListingUnavailable", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRepositoryCreateAppliesRenterGrowthDiscount(t *testing.T) {
|
func TestRepositoryCreateAppliesRenterGrowthDiscount(t *testing.T) {
|
||||||
db := setupOrderTestDB(t)
|
db := setupOrderTestDB(t)
|
||||||
repo := NewRepository(db)
|
repo := NewRepository(db)
|
||||||
@@ -664,6 +721,100 @@ func TestCancelClosesPendingPaymentOrder(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCancelDoesNotReleaseAssetsWhenSiblingOrderActive(t *testing.T) {
|
||||||
|
db := setupOrderTestDB(t)
|
||||||
|
repo := NewRepository(db)
|
||||||
|
|
||||||
|
owner := model.User{Phone: "13800002001"}
|
||||||
|
activeRenter := model.User{Phone: "13800002002"}
|
||||||
|
cancelRenter := model.User{Phone: "13800002003"}
|
||||||
|
if err := db.Create(&owner).Error; err != nil {
|
||||||
|
t.Fatalf("create owner failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&activeRenter).Error; err != nil {
|
||||||
|
t.Fatalf("create active renter failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&cancelRenter).Error; err != nil {
|
||||||
|
t.Fatalf("create cancel renter failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
account := model.GameAccount{
|
||||||
|
OwnerID: owner.ID,
|
||||||
|
ServerRegion: "国服",
|
||||||
|
LoginPlatform: "steam",
|
||||||
|
Title: "测试账号",
|
||||||
|
Status: accountStatusRented,
|
||||||
|
}
|
||||||
|
if err := db.Create(&account).Error; err != nil {
|
||||||
|
t.Fatalf("create account failed: %v", err)
|
||||||
|
}
|
||||||
|
listing := model.RentalListing{
|
||||||
|
AccountID: account.ID,
|
||||||
|
OwnerID: owner.ID,
|
||||||
|
PriceCent: 1000,
|
||||||
|
Status: listingStatusRented,
|
||||||
|
ReviewStatus: listingReviewStatusApproved,
|
||||||
|
InTransaction: true,
|
||||||
|
}
|
||||||
|
if err := db.Create(&listing).Error; err != nil {
|
||||||
|
t.Fatalf("create listing failed: %v", err)
|
||||||
|
}
|
||||||
|
activeOrder := model.RentalOrder{
|
||||||
|
OrderNo: "ORD-ACTIVE-SIBLING-002",
|
||||||
|
ListingID: listing.ID,
|
||||||
|
AccountID: account.ID,
|
||||||
|
OwnerID: owner.ID,
|
||||||
|
RenterID: activeRenter.ID,
|
||||||
|
Status: orderStatusRenting,
|
||||||
|
HandoffStatus: handoffStatusReceived,
|
||||||
|
}
|
||||||
|
if err := db.Create(&activeOrder).Error; err != nil {
|
||||||
|
t.Fatalf("create active order failed: %v", err)
|
||||||
|
}
|
||||||
|
cancelOrder := model.RentalOrder{
|
||||||
|
OrderNo: "ORD-CANCEL-SIBLING-001",
|
||||||
|
ListingID: listing.ID,
|
||||||
|
AccountID: account.ID,
|
||||||
|
OwnerID: owner.ID,
|
||||||
|
RenterID: cancelRenter.ID,
|
||||||
|
RentAmountCent: 1000,
|
||||||
|
Status: orderStatusPendingHandoff,
|
||||||
|
HandoffStatus: handoffStatusPendingOwner,
|
||||||
|
}
|
||||||
|
if err := db.Create(&cancelOrder).Error; err != nil {
|
||||||
|
t.Fatalf("create cancel order failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := repo.Cancel(t.Context(), cancelRenter.ID, cancelOrder.ID); err != nil {
|
||||||
|
t.Fatalf("Cancel() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var savedListing model.RentalListing
|
||||||
|
if err := db.First(&savedListing, listing.ID).Error; err != nil {
|
||||||
|
t.Fatalf("find listing failed: %v", err)
|
||||||
|
}
|
||||||
|
if savedListing.Status != listingStatusRented {
|
||||||
|
t.Fatalf("listing status = %q, want %q", savedListing.Status, listingStatusRented)
|
||||||
|
}
|
||||||
|
if !savedListing.InTransaction {
|
||||||
|
t.Fatal("listing InTransaction = false, want true")
|
||||||
|
}
|
||||||
|
var savedAccount model.GameAccount
|
||||||
|
if err := db.First(&savedAccount, account.ID).Error; err != nil {
|
||||||
|
t.Fatalf("find account failed: %v", err)
|
||||||
|
}
|
||||||
|
if savedAccount.Status != accountStatusRented {
|
||||||
|
t.Fatalf("account status = %q, want %q", savedAccount.Status, accountStatusRented)
|
||||||
|
}
|
||||||
|
var savedCancelOrder model.RentalOrder
|
||||||
|
if err := db.First(&savedCancelOrder, cancelOrder.ID).Error; err != nil {
|
||||||
|
t.Fatalf("find cancel order failed: %v", err)
|
||||||
|
}
|
||||||
|
if savedCancelOrder.Status != orderStatusCancelled {
|
||||||
|
t.Fatalf("cancel order status = %q, want %q", savedCancelOrder.Status, orderStatusCancelled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAdminResetCheckoutConfirmTimeout(t *testing.T) {
|
func TestAdminResetCheckoutConfirmTimeout(t *testing.T) {
|
||||||
db := setupOrderTestDB(t)
|
db := setupOrderTestDB(t)
|
||||||
repo := NewRepository(db)
|
repo := NewRepository(db)
|
||||||
|
|||||||
Reference in New Issue
Block a user