Skip to content

Commit 8f60dfa

Browse files
authored
Merge pull request #45 from Zenfulcode/development
Deleting checkouts command and item deletion fix
2 parents c497ec5 + 57eb17b commit 8f60dfa

9 files changed

Lines changed: 461 additions & 12 deletions

File tree

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,6 @@ mod-tidy: ## Tidy Go modules
129129
# Maintenance commands
130130
expire-checkouts: ## Expire old checkouts manually
131131
go run ./cmd/expire-checkouts
132+
133+
force-delete-checkouts: ## Force delete all expired, abandoned, and old completed checkouts
134+
go run ./cmd/expire-checkouts -force

cmd/expire-checkouts/main.go

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"flag"
45
"log"
56

67
"github.com/joho/godotenv"
@@ -11,14 +12,22 @@ import (
1112
)
1213

1314
func main() {
15+
// Parse command line flags
16+
forceDelete := flag.Bool("force", false, "Force delete all expired, abandoned, and old completed checkouts")
17+
flag.Parse()
18+
1419
// Load environment variables
1520
if err := godotenv.Load(); err != nil {
1621
log.Println("No .env file found, using environment variables")
1722
}
1823

1924
// Initialize logger
2025
logger := logger.NewLogger()
21-
logger.Info("Starting checkout expiry cleanup tool")
26+
if *forceDelete {
27+
logger.Info("Starting checkout force deletion tool")
28+
} else {
29+
logger.Info("Starting checkout expiry cleanup tool")
30+
}
2231

2332
// Load configuration
2433
cfg, err := config.LoadConfig()
@@ -39,15 +48,27 @@ func main() {
3948
// Get checkout use case
4049
checkoutUseCase := diContainer.UseCases().CheckoutUseCase()
4150

42-
// Expire old checkouts
43-
result, err := checkoutUseCase.ExpireOldCheckouts()
44-
if err != nil {
45-
logger.Fatal("Failed to expire old checkouts: %v", err)
46-
}
51+
if *forceDelete {
52+
// Force delete all expired checkouts
53+
deleteResult, err := checkoutUseCase.ForceDeleteAllExpiredCheckouts()
54+
if err != nil {
55+
logger.Fatal("Failed to force delete expired checkouts: %v", err)
56+
}
57+
58+
logger.Info("Force deletion completed:")
59+
logger.Info("- Force deleted checkouts: %d", deleteResult.DeletedCount)
60+
logger.Info("Total processed: %d", deleteResult.DeletedCount)
61+
} else {
62+
// Regular expire old checkouts
63+
expireResult, err := checkoutUseCase.ExpireOldCheckouts()
64+
if err != nil {
65+
logger.Fatal("Failed to expire old checkouts: %v", err)
66+
}
4767

48-
logger.Info("Checkout cleanup completed:")
49-
logger.Info("- Abandoned checkouts: %d", result.AbandonedCount)
50-
logger.Info("- Deleted checkouts: %d", result.DeletedCount)
51-
logger.Info("- Expired checkouts: %d", result.ExpiredCount)
52-
logger.Info("Total processed: %d", result.AbandonedCount+result.DeletedCount+result.ExpiredCount)
68+
logger.Info("Checkout cleanup completed:")
69+
logger.Info("- Abandoned checkouts: %d", expireResult.AbandonedCount)
70+
logger.Info("- Deleted checkouts: %d", expireResult.DeletedCount)
71+
logger.Info("- Expired checkouts: %d", expireResult.ExpiredCount)
72+
logger.Info("Total processed: %d", expireResult.AbandonedCount+expireResult.DeletedCount+expireResult.ExpiredCount)
73+
}
5374
}

internal/application/usecase/checkout_usecase.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,28 @@ func (uc *CheckoutUseCase) ExpireOldCheckoutsLegacy() (int, error) {
572572
return result.AbandonedCount + result.DeletedCount + result.ExpiredCount, nil
573573
}
574574

575+
// ForceDeleteAllExpiredCheckouts forcefully deletes all expired, abandoned, and old completed checkouts
576+
func (uc *CheckoutUseCase) ForceDeleteAllExpiredCheckouts() (*CheckoutCleanupResult, error) {
577+
result := &CheckoutCleanupResult{}
578+
579+
// Get all expired checkouts for deletion
580+
checkoutsToDelete, err := uc.checkoutRepo.GetAllExpiredCheckoutsForDeletion()
581+
if err != nil {
582+
return result, fmt.Errorf("failed to get expired checkouts for deletion: %w", err)
583+
}
584+
585+
for _, checkout := range checkoutsToDelete {
586+
err = uc.checkoutRepo.Delete(checkout.ID)
587+
if err != nil {
588+
log.Printf("Failed to force delete checkout %d: %v", checkout.ID, err)
589+
continue
590+
}
591+
result.DeletedCount++
592+
}
593+
594+
return result, nil
595+
}
596+
575597
// CreateOrderFromCheckout creates an order from a checkout
576598
func (uc *CheckoutUseCase) CreateOrderFromCheckout(checkoutID uint) (*entity.Order, error) {
577599
// Get checkout
@@ -780,6 +802,26 @@ func (uc *CheckoutUseCase) GetOrCreateCheckoutBySessionIDWithCurrency(sessionID
780802
return checkout, nil
781803
}
782804

805+
// If no active checkout found, try to get an abandoned checkout and reactivate it
806+
abandonedCheckout, err := uc.checkoutRepo.GetAbandonedBySessionID(sessionID)
807+
if err == nil {
808+
// Reactivate the abandoned checkout
809+
abandonedCheckout.Reactivate()
810+
811+
// If currency is specified, change currency if different
812+
if currency != "" && abandonedCheckout.Currency != currency {
813+
return uc.ChangeCurrency(abandonedCheckout, currency)
814+
}
815+
816+
// Update the checkout in repository
817+
err = uc.checkoutRepo.Update(abandonedCheckout)
818+
if err != nil {
819+
return nil, fmt.Errorf("failed to reactivate abandoned checkout: %w", err)
820+
}
821+
822+
return abandonedCheckout, nil
823+
}
824+
783825
// If not found, create a new one
784826
var checkoutCurrency string
785827
if currency != "" {

internal/domain/entity/checkout.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,12 @@ func (c *Checkout) MarkAsExpired() {
367367
c.LastActivityAt = time.Now()
368368
}
369369

370+
// Reactivate marks an abandoned checkout as active again
371+
func (c *Checkout) Reactivate() {
372+
c.Status = CheckoutStatusActive
373+
c.LastActivityAt = time.Now()
374+
}
375+
370376
// IsExpired checks if the checkout has expired
371377
func (c *Checkout) IsExpired() bool {
372378
return time.Now().After(c.ExpiresAt)

internal/domain/entity/checkout_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,37 @@ func TestCheckoutStatus(t *testing.T) {
323323
assert.Equal(t, CheckoutStatus("abandoned"), CheckoutStatusAbandoned)
324324
assert.Equal(t, CheckoutStatus("expired"), CheckoutStatusExpired)
325325
})
326+
327+
t.Run("MarkAsAbandoned", func(t *testing.T) {
328+
checkout, err := NewCheckout("session123", "USD")
329+
require.NoError(t, err)
330+
331+
originalTime := checkout.LastActivityAt
332+
time.Sleep(1 * time.Millisecond) // Ensure time difference
333+
334+
checkout.MarkAsAbandoned()
335+
336+
assert.Equal(t, CheckoutStatusAbandoned, checkout.Status)
337+
assert.True(t, checkout.LastActivityAt.After(originalTime))
338+
})
339+
340+
t.Run("Reactivate", func(t *testing.T) {
341+
checkout, err := NewCheckout("session123", "USD")
342+
require.NoError(t, err)
343+
344+
// First mark as abandoned
345+
checkout.MarkAsAbandoned()
346+
assert.Equal(t, CheckoutStatusAbandoned, checkout.Status)
347+
348+
originalTime := checkout.LastActivityAt
349+
time.Sleep(1 * time.Millisecond) // Ensure time difference
350+
351+
// Then reactivate
352+
checkout.Reactivate()
353+
354+
assert.Equal(t, CheckoutStatusActive, checkout.Status)
355+
assert.True(t, checkout.LastActivityAt.After(originalTime))
356+
})
326357
}
327358

328359
func TestCheckoutDTOConversions(t *testing.T) {

internal/domain/repository/checkout_repository.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ type CheckoutRepository interface {
1616
// GetBySessionID retrieves an active checkout by session ID
1717
GetBySessionID(sessionID string) (*entity.Checkout, error)
1818

19+
// GetAbandonedBySessionID retrieves an abandoned checkout by session ID
20+
GetAbandonedBySessionID(sessionID string) (*entity.Checkout, error)
21+
1922
// Update updates a checkout
2023
Update(checkout *entity.Checkout) error
2124

@@ -45,4 +48,7 @@ type CheckoutRepository interface {
4548

4649
// HasActiveCheckoutsWithProduct checks if a product has any active checkouts
4750
HasActiveCheckoutsWithProduct(productID uint) (bool, error)
51+
52+
// GetAllExpiredCheckoutsForDeletion retrieves all expired checkouts for force deletion
53+
GetAllExpiredCheckoutsForDeletion() ([]*entity.Checkout, error)
4854
}

internal/infrastructure/repository/gorm/checkout_repository.go

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,19 @@ func (c *CheckoutRepository) GetBySessionID(sessionID string) (*entity.Checkout,
8888
return &checkout, nil
8989
}
9090

91+
// GetAbandonedBySessionID implements repository.CheckoutRepository.
92+
func (c *CheckoutRepository) GetAbandonedBySessionID(sessionID string) (*entity.Checkout, error) {
93+
var checkout entity.Checkout
94+
err := c.db.Preload("Items").Preload("Items.Product").Preload("Items.ProductVariant").
95+
Preload("User").
96+
Where("session_id = ? AND status = ?", sessionID, entity.CheckoutStatusAbandoned).
97+
First(&checkout).Error
98+
if err != nil {
99+
return nil, err
100+
}
101+
return &checkout, nil
102+
}
103+
91104
// GetByUserID implements repository.CheckoutRepository.
92105
func (c *CheckoutRepository) GetByUserID(userID uint) (*entity.Checkout, error) {
93106
var checkout entity.Checkout
@@ -207,7 +220,55 @@ func (c *CheckoutRepository) HasActiveCheckoutsWithProduct(productID uint) (bool
207220

208221
// Update implements repository.CheckoutRepository.
209222
func (c *CheckoutRepository) Update(checkout *entity.Checkout) error {
210-
return c.db.Session(&gorm.Session{FullSaveAssociations: true}).Save(checkout).Error
223+
return c.db.Transaction(func(tx *gorm.DB) error {
224+
// First, get the current items in the database
225+
var currentItems []entity.CheckoutItem
226+
if err := tx.Where("checkout_id = ?", checkout.ID).Find(&currentItems).Error; err != nil {
227+
return fmt.Errorf("failed to fetch current checkout items: %w", err)
228+
}
229+
230+
// Create a map of current item IDs in the checkout entity for efficient lookup
231+
currentItemIDs := make(map[uint]bool)
232+
for _, item := range checkout.Items {
233+
if item.ID != 0 {
234+
currentItemIDs[item.ID] = true
235+
}
236+
}
237+
238+
// Delete items that are no longer in the checkout
239+
for _, dbItem := range currentItems {
240+
if !currentItemIDs[dbItem.ID] {
241+
if err := tx.Unscoped().Delete(&dbItem).Error; err != nil {
242+
return fmt.Errorf("failed to delete checkout item %d: %w", dbItem.ID, err)
243+
}
244+
}
245+
}
246+
247+
// Save the checkout with remaining items
248+
return tx.Session(&gorm.Session{FullSaveAssociations: true}).Save(checkout).Error
249+
})
250+
}
251+
252+
// GetAllExpiredCheckoutsForDeletion implements repository.CheckoutRepository.
253+
func (c *CheckoutRepository) GetAllExpiredCheckoutsForDeletion() ([]*entity.Checkout, error) {
254+
var checkouts []*entity.Checkout
255+
256+
// Get all checkouts that are expired, abandoned, or completed (older than 30 days to be safe)
257+
thirtyDaysAgo := time.Now().AddDate(0, 0, -30)
258+
259+
err := c.db.Preload("Items").Preload("Items.Product").Preload("Items.ProductVariant").
260+
Where("status IN (?, ?, ?) OR updated_at < ?",
261+
entity.CheckoutStatusExpired,
262+
entity.CheckoutStatusAbandoned,
263+
entity.CheckoutStatusCompleted,
264+
thirtyDaysAgo).
265+
Find(&checkouts).Error
266+
267+
if err != nil {
268+
return nil, fmt.Errorf("failed to get expired checkouts for deletion: %w", err)
269+
}
270+
271+
return checkouts, nil
211272
}
212273

213274
// NewCheckoutRepository creates a new GORM-based CheckoutRepository

0 commit comments

Comments
 (0)