mirror of
https://github.com/marcusDenslow/terminalShop.git
synced 2026-10-25 18:08:45 +01:00
When paying the application charges the card and then sends a payment record to the database after, in a situation where something goes wrong between those two steps, we could potentially end up where a users card was charged but no record on what is was charged for. The fix is to re-order ConvertCart to write a pending order record before calling Stripe, this way a DB record always exists if anything fails mid-operation. This implementation uses an idempotency key keyed to the order ID to prevent double-charges on retry. Wraps the post-charge DB updates in a transaction so marking paid andd clearing the cart are atomic. Adds OrderStatusFailed and a CRITICAL log for the unrecoverable case. Includes a checkout flow doc with test cards and reconcilation steps.
80 lines
3.2 KiB
Go
80 lines
3.2 KiB
Go
package models
|
|
|
|
import (
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// OrderStatus represents the lifecycle of an order.
|
|
type OrderStatus string
|
|
|
|
const (
|
|
OrderStatusPending OrderStatus = "pending"
|
|
OrderStatusPaid OrderStatus = "paid"
|
|
OrderStatusShipped OrderStatus = "shipped"
|
|
OrderStatusDelivered OrderStatus = "delivered"
|
|
OrderStatusCancelled OrderStatus = "cancelled"
|
|
OrderStatusRefunded OrderStatus = "refunded"
|
|
OrderStatusFailed OrderStatus = "failed"
|
|
)
|
|
|
|
// Order represents a completed purchase.
|
|
type Order struct {
|
|
ID uint `gorm:"primaryKey" json:"id"`
|
|
UserID uint `gorm:"not null;index" json:"user_id"`
|
|
CardID uint `gorm:"not null" json:"card_id"`
|
|
StripePaymentID string `gorm:"size:255" json:"-"` // Stripe PaymentIntent ID
|
|
Status OrderStatus `gorm:"size:50;not null;default:'pending'" json:"status"`
|
|
Subtotal int `gorm:"not null" json:"subtotal"` // Amount in cents
|
|
ShippingCost int `gorm:"default:0" json:"shipping_cost"` // Amount in cents
|
|
Total int `gorm:"not null" json:"total"` // Amount in cents
|
|
ShippingName string `gorm:"size:255" json:"shipping_name"`
|
|
ShippingStreet string `gorm:"size:255" json:"shipping_street"`
|
|
ShippingStreet2 string `gorm:"size:255" json:"shipping_street2"`
|
|
ShippingCity string `gorm:"size:255" json:"shipping_city"`
|
|
ShippingState string `gorm:"size:50" json:"shipping_state"`
|
|
ShippingZip string `gorm:"size:20" json:"shipping_zip"`
|
|
ShippingCountry string `gorm:"size:2" json:"shipping_country"`
|
|
ShippingPhone string `gorm:"size:20" json:"shipping_phone"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
|
|
|
// Associations
|
|
User User `gorm:"foreignKey:UserID" json:"-"`
|
|
Card Card `gorm:"foreignKey:CardID" json:"-"`
|
|
Items []OrderItem `gorm:"foreignKey:OrderID" json:"items"`
|
|
}
|
|
|
|
// TableName specifies the table name for the Order model.
|
|
func (Order) TableName() string {
|
|
return "orders"
|
|
}
|
|
|
|
// OrderItem represents a single line item within an order.
|
|
type OrderItem struct {
|
|
ID uint `gorm:"primaryKey" json:"id"`
|
|
OrderID uint `gorm:"not null;index" json:"order_id"`
|
|
CoffeeID uint `gorm:"not null" json:"coffee_id"`
|
|
Name string `gorm:"size:255;not null" json:"name"` // Snapshot of coffee name at time of order
|
|
Price int `gorm:"not null" json:"price"` // Price in cents at time of order
|
|
Quantity int `gorm:"not null" json:"quantity"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
|
|
|
// Associations
|
|
Order Order `gorm:"foreignKey:OrderID" json:"-"`
|
|
Coffee Coffee `gorm:"foreignKey:CoffeeID" json:"-"`
|
|
}
|
|
|
|
// TableName specifies the table name for the OrderItem model.
|
|
func (OrderItem) TableName() string {
|
|
return "order_items"
|
|
}
|
|
|
|
// TotalInDollars returns the order total formatted as a float for display.
|
|
func (o *Order) TotalInDollars() float64 {
|
|
return float64(o.Total) / 100
|
|
}
|