fix: payment validation — tip cap ValidateAmount, RefundRequest gt=0 tag, float64 pence conversion docs
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -150,7 +150,7 @@ type CreateBookingPaymentRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RefundRequest struct {
|
type RefundRequest struct {
|
||||||
Amount int64 `json:"amount"`
|
Amount int64 `json:"amount" validate:"required,gt=0"`
|
||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
// Optional client-generated idempotency key. Two DISTINCT refunds of the
|
// Optional client-generated idempotency key. Two DISTINCT refunds of the
|
||||||
// same amount against the same payment must not collide on the default
|
// same amount against the same payment must not collide on the default
|
||||||
@@ -4900,6 +4900,12 @@ func CreateTipPayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := ValidateAmount(req.Amount); err != nil {
|
||||||
|
log.Printf("Failed to process request: %v", err)
|
||||||
|
http.Error(w, "Invalid request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Product rule (security): only verified accounts may save cards. An
|
// Product rule (security): only verified accounts may save cards. An
|
||||||
// unverified/guest/affiliate user may still tip, but save_card=true is
|
// unverified/guest/affiliate user may still tip, but save_card=true is
|
||||||
// rejected here — before any charge source resolution or payment record.
|
// rejected here — before any charge source resolution or payment record.
|
||||||
@@ -4935,12 +4941,6 @@ func CreateTipPayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ValidateAmount(req.Amount); err != nil {
|
|
||||||
log.Printf("Failed to process request: %v", err)
|
|
||||||
http.Error(w, "Invalid request", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// maxOnlineTipPence bound: ValidateAmount's generic £10,000 cap is the
|
// maxOnlineTipPence bound: ValidateAmount's generic £10,000 cap is the
|
||||||
// money-minting ceiling for booking charges, but a tip is gratuity on a
|
// money-minting ceiling for booking charges, but a tip is gratuity on a
|
||||||
// percentage of the service — a single online tip over £250 is not a
|
// percentage of the service — a single online tip over £250 is not a
|
||||||
|
|||||||
@@ -762,6 +762,8 @@ func scanStaleRow(table string, rows pgx.Rows) (staleRow, error) {
|
|||||||
r.IsCreate = isCreate != nil && *isCreate
|
r.IsCreate = isCreate != nil && *isCreate
|
||||||
r.HasGiftCard = hasGiftCard
|
r.HasGiftCard = hasGiftCard
|
||||||
r.AmountPence = int64(math.Round(r.TotalAmount * 100))
|
r.AmountPence = int64(math.Round(r.TotalAmount * 100))
|
||||||
|
// NOTE: float64→int64 pence conversion can drift by ±1p between charge and replay.
|
||||||
|
// A full fix would store pence as int64 throughout (out of scope for this change).
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
var amount float64
|
var amount float64
|
||||||
@@ -771,6 +773,8 @@ func scanStaleRow(table string, rows pgx.Rows) (staleRow, error) {
|
|||||||
}
|
}
|
||||||
r.SquareRequestSnapshot = []byte(snapshot.String)
|
r.SquareRequestSnapshot = []byte(snapshot.String)
|
||||||
r.AmountPence = int64(math.Round(amount * 100))
|
r.AmountPence = int64(math.Round(amount * 100))
|
||||||
|
// NOTE: float64→int64 pence conversion can drift by ±1p between charge and replay.
|
||||||
|
// A full fix would store pence as int64 throughout (out of scope for this change).
|
||||||
if bookingID.Valid && bookingID.String != "" {
|
if bookingID.Valid && bookingID.String != "" {
|
||||||
b := bookingID.String
|
b := bookingID.String
|
||||||
r.BookingID = &b
|
r.BookingID = &b
|
||||||
|
|||||||
@@ -197,6 +197,8 @@ func deriveTillIdempotencyKey(req TillSaleRequest, adminID string) string {
|
|||||||
sb.WriteString(adminID)
|
sb.WriteString(adminID)
|
||||||
sb.WriteString(":")
|
sb.WriteString(":")
|
||||||
sb.WriteString(strconv.FormatInt(int64(math.Round(req.Amount*100)), 10))
|
sb.WriteString(strconv.FormatInt(int64(math.Round(req.Amount*100)), 10))
|
||||||
|
// NOTE: float64→int64 pence conversion can drift by ±1p between charge and replay.
|
||||||
|
// A full fix would store pence as int64 throughout (out of scope for this change).
|
||||||
if req.GiftCardID != nil && *req.GiftCardID != "" {
|
if req.GiftCardID != nil && *req.GiftCardID != "" {
|
||||||
sb.WriteString(":gc:")
|
sb.WriteString(":gc:")
|
||||||
sb.WriteString(validators.NormalizeGiftCardCode(*req.GiftCardID))
|
sb.WriteString(validators.NormalizeGiftCardCode(*req.GiftCardID))
|
||||||
@@ -610,6 +612,8 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) {
|
|||||||
// the effective pence figure exactly as the Square charge amount below is
|
// the effective pence figure exactly as the Square charge amount below is
|
||||||
// derived (math.Round(req.Amount * 100)).
|
// derived (math.Round(req.Amount * 100)).
|
||||||
amountPence := int64(math.Round(req.Amount * 100))
|
amountPence := int64(math.Round(req.Amount * 100))
|
||||||
|
// NOTE: float64→int64 pence conversion can drift by ±1p between charge and replay.
|
||||||
|
// A full fix would store pence as int64 throughout (out of scope for this change).
|
||||||
// Gift-card creates/topups are additionally capped at £250 per transaction
|
// Gift-card creates/topups are additionally capped at £250 per transaction
|
||||||
// (owner decision — the shared maxAdminGiftCardTransactionPence from
|
// (owner decision — the shared maxAdminGiftCardTransactionPence from
|
||||||
// giftcard_limits.go, the single source of the £250 cap). Both the create
|
// giftcard_limits.go, the single source of the £250 cap). Both the create
|
||||||
|
|||||||
Reference in New Issue
Block a user