fix: payment API amount precision (pence), partial validation, and create payment method endpoint
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -212,7 +213,7 @@ func GetCheckoutStatus(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(PaymentStatusResponse{
|
||||
Status: "COMPLETED",
|
||||
PaymentID: existing.ID,
|
||||
Amount: existing.Amount,
|
||||
Amount: int64(existing.Amount * 100),
|
||||
CardBrand: paymentResult.CardBrand,
|
||||
CardLast4: paymentResult.CardLast4,
|
||||
ReceiptURL: paymentResult.ReceiptURL,
|
||||
@@ -227,10 +228,10 @@ func GetCheckoutStatus(w http.ResponseWriter, r *http.Request) {
|
||||
PaymentType: "full",
|
||||
PaymentMethod: "in_person_card",
|
||||
Status: "completed",
|
||||
Amount: paymentResult.Amount,
|
||||
Amount: float64(paymentResult.Amount) / 100.0,
|
||||
SquarePaymentID: &paymentResult.SquarePayID,
|
||||
IdempotencyKey: &idempotencyKey,
|
||||
Fees: paymentResult.Fees,
|
||||
Fees: float64(paymentResult.Fees) / 100.0,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
@@ -294,6 +295,19 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
service := NewPaymentService()
|
||||
|
||||
if req.PaymentType == "partial" {
|
||||
remainingCents, err := service.GetBookingRemainingBalanceCents(r.Context(), bookingID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get remaining balance: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := ValidatePartialAmount(req.Amount, remainingCents); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
bookingUserID, err := service.GetBookingUserID(r.Context(), bookingID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
@@ -321,7 +335,7 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
|
||||
BookingID: existingPayment.BookingID,
|
||||
PaymentType: existingPayment.PaymentType,
|
||||
Status: existingPayment.Status,
|
||||
Amount: existingPayment.Amount,
|
||||
Amount: int64(existingPayment.Amount * 100),
|
||||
CreatedAt: existingPayment.CreatedAt.Format(time.RFC3339),
|
||||
})
|
||||
return
|
||||
@@ -386,10 +400,10 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
|
||||
PaymentType: req.PaymentType,
|
||||
PaymentMethod: "online_square",
|
||||
Status: "completed",
|
||||
Amount: req.Amount,
|
||||
Amount: float64(req.Amount) / 100.0,
|
||||
SquarePaymentID: &paymentResult.SquarePayID,
|
||||
IdempotencyKey: &req.IdempotencyKey,
|
||||
Fees: fees,
|
||||
Fees: float64(fees) / 100.0,
|
||||
UserSavedCardID: savedCardID,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
@@ -468,6 +482,46 @@ func DeletePaymentMethod(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "deleted"})
|
||||
}
|
||||
|
||||
type CreatePaymentMethodRequest struct {
|
||||
CardNumber string `json:"card_number"`
|
||||
Expiry string `json:"expiry"`
|
||||
CVC string `json:"cvc"`
|
||||
}
|
||||
|
||||
func CreatePaymentMethod(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := r.Context().Value(mw.UserIDKey).(string)
|
||||
if !ok || userID == "" {
|
||||
http.Error(w, "Authentication required", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var req CreatePaymentMethodRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.CardNumber == "" || req.Expiry == "" || req.CVC == "" {
|
||||
http.Error(w, "Card number, expiry, and CVC are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
service := NewPaymentService()
|
||||
card, err := service.CreatePaymentMethodFromDetails(r.Context(), userID, req.CardNumber, req.Expiry, req.CVC)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "invalid") || strings.Contains(err.Error(), "expired") {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
log.Printf("Failed to create payment method: %v", err)
|
||||
http.Error(w, "Failed to add card", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(card)
|
||||
}
|
||||
|
||||
func RefundPayment(w http.ResponseWriter, r *http.Request) {
|
||||
paymentID := chi.URLParam(r, "payment_id")
|
||||
if paymentID == "" {
|
||||
@@ -528,7 +582,7 @@ func RefundPayment(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Amount+alreadyRefunded > payment.Amount {
|
||||
if req.Amount+alreadyRefunded > int64(payment.Amount*100) {
|
||||
http.Error(w, "Refund amount exceeds payment amount", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -551,7 +605,7 @@ func RefundPayment(w http.ResponseWriter, r *http.Request) {
|
||||
record := RefundRecord{
|
||||
PaymentID: paymentID,
|
||||
BookingID: payment.BookingID,
|
||||
Amount: req.Amount,
|
||||
Amount: float64(req.Amount) / 100.0,
|
||||
SquareRefundID: &squareRefundID,
|
||||
Status: "completed",
|
||||
Reason: req.Reason,
|
||||
@@ -566,7 +620,7 @@ func RefundPayment(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if payment.PaymentType == "deposit" && (req.Amount+alreadyRefunded) >= payment.Amount {
|
||||
if payment.PaymentType == "deposit" && (req.Amount+alreadyRefunded) >= int64(payment.Amount*100) {
|
||||
err = service.UpdateBookingDepositPaid(r.Context(), payment.BookingID, false)
|
||||
if err != nil {
|
||||
log.Printf("Failed to update deposit paid: %v", err)
|
||||
@@ -667,7 +721,7 @@ func CreateTipPayment(w http.ResponseWriter, r *http.Request) {
|
||||
PaymentType: "tip",
|
||||
PaymentMethod: "online_square",
|
||||
Status: "completed",
|
||||
Amount: req.Amount,
|
||||
Amount: float64(req.Amount) / 100.0,
|
||||
SquarePaymentID: &paymentResult.SquarePayID,
|
||||
IdempotencyKey: &idempotencyKey,
|
||||
Fees: 0,
|
||||
@@ -741,7 +795,8 @@ func GetBookingPaymentSummary(w http.ResponseWriter, r *http.Request) {
|
||||
BookingID: p.BookingID,
|
||||
PaymentType: p.PaymentType,
|
||||
Status: p.Status,
|
||||
Amount: p.Amount,
|
||||
Amount: int64(p.Amount * 100),
|
||||
CardLast4: p.CardLast4,
|
||||
CreatedAt: p.CreatedAt.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
@@ -751,7 +806,7 @@ func GetBookingPaymentSummary(w http.ResponseWriter, r *http.Request) {
|
||||
refunds[i] = RefundResponse{
|
||||
ID: rf.ID,
|
||||
PaymentID: rf.PaymentID,
|
||||
Amount: rf.Amount,
|
||||
Amount: int64(rf.Amount * 100),
|
||||
Status: rf.Status,
|
||||
Reason: rf.Reason,
|
||||
CreatedAt: rf.CreatedAt.Format(time.RFC3339),
|
||||
@@ -760,10 +815,10 @@ func GetBookingPaymentSummary(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(PaymentSummaryResponse{
|
||||
TotalAmount: summary.TotalAmount,
|
||||
PaidAmount: summary.PaidAmount,
|
||||
RefundedAmount: summary.RefundedAmount,
|
||||
RemainingAmount: summary.RemainingAmount,
|
||||
TotalAmount: int64(summary.TotalAmount * 100),
|
||||
PaidAmount: int64(summary.PaidAmount * 100),
|
||||
RefundedAmount: int64(summary.RefundedAmount * 100),
|
||||
RemainingAmount: int64(summary.RemainingAmount * 100),
|
||||
Payments: payments,
|
||||
Refunds: refunds,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user