feat(backend): update Square integration, validators, and image validation

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-06-18 16:26:47 +01:00
co-authored by Sisyphus
parent bc6a5461c1
commit 2dbb1486b0
7 changed files with 91 additions and 34 deletions
+4 -4
View File
@@ -166,10 +166,10 @@ func TestValidateImageBytes_Empty(t *testing.T) {
// TestValidateImageBytes_AllSupportedFormats verifies every supported format returns a non-empty extension. // TestValidateImageBytes_AllSupportedFormats verifies every supported format returns a non-empty extension.
func TestValidateImageBytes_AllSupportedFormats(t *testing.T) { func TestValidateImageBytes_AllSupportedFormats(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
data []byte data []byte
wantExt string wantExt string
wantErr bool wantErr bool
}{ }{
{"JPEG", []byte{0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01}, ".jpg", false}, {"JPEG", []byte{0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01}, ".jpg", false},
{"PNG", []byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n', 0, 0, 0, 0}, ".png", false}, {"PNG", []byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n', 0, 0, 0, 0}, ".png", false},
+18 -10
View File
@@ -14,13 +14,21 @@ import (
var Client SquareClient var Client SquareClient
var isTesting = os.Getenv("GO_TESTING") == "1"
func mockSleep(d time.Duration) {
if !isTesting {
time.Sleep(d)
}
}
type MockClient struct { type MockClient struct {
mu sync.RWMutex mu sync.RWMutex
cards map[string]map[string]*CardOnFile cards map[string]map[string]*CardOnFile
checkouts map[string]*CheckoutResult checkouts map[string]*CheckoutResult
payments map[string]*PaymentResult payments map[string]*PaymentResult
refunds map[string]*RefundResult refunds map[string]*RefundResult
completed map[string]*PaymentResult completed map[string]*PaymentResult
} }
type devProdClient struct{} type devProdClient struct{}
@@ -72,7 +80,7 @@ func NewDevClient() SquareClient {
func (m *MockClient) CreatePayment(ctx context.Context, req CreatePaymentReq) (*PaymentResult, error) { func (m *MockClient) CreatePayment(ctx context.Context, req CreatePaymentReq) (*PaymentResult, error) {
log.Printf("[SQUARE-MOCK] CreatePayment: amount=%d, reference=%s", req.Amount, req.ReferenceID) log.Printf("[SQUARE-MOCK] CreatePayment: amount=%d, reference=%s", req.Amount, req.ReferenceID)
time.Sleep(1 * time.Second) mockSleep(1 * time.Second)
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
@@ -110,7 +118,7 @@ func (m *MockClient) CreateCheckout(ctx context.Context, req CreateCheckoutReq)
m.mu.Unlock() m.mu.Unlock()
go func() { go func() {
time.Sleep(3 * time.Second) mockSleep(3 * time.Second)
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
@@ -122,7 +130,7 @@ func (m *MockClient) CreateCheckout(ctx context.Context, req CreateCheckoutReq)
tipAmount = 500 tipAmount = 500
amount += tipAmount amount += tipAmount
} }
fees := amount*175/10000 // in-person rate: 1.75% fees := amount * 175 / 10000 // in-person rate: 1.75%
paymentResult := &PaymentResult{ paymentResult := &PaymentResult{
ID: paymentID, ID: paymentID,
@@ -171,7 +179,7 @@ func (m *MockClient) GetCheckout(ctx context.Context, checkoutID string) (*Payme
func (m *MockClient) RefundPayment(ctx context.Context, req RefundPaymentReq) (*RefundResult, error) { func (m *MockClient) RefundPayment(ctx context.Context, req RefundPaymentReq) (*RefundResult, error) {
log.Printf("[SQUARE-MOCK] RefundPayment: payment=%s, amount=%d", req.PaymentID, req.Amount) log.Printf("[SQUARE-MOCK] RefundPayment: payment=%s, amount=%d", req.PaymentID, req.Amount)
time.Sleep(1 * time.Second) mockSleep(1 * time.Second)
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
+16 -16
View File
@@ -27,15 +27,15 @@ type RefundPaymentReq struct {
} }
type PaymentResult struct { type PaymentResult struct {
ID string ID string
Status string // "COMPLETED", "FAILED", "PENDING" Status string // "COMPLETED", "FAILED", "PENDING"
Amount int64 Amount int64
CardBrand string CardBrand string
CardLast4 string CardLast4 string
TipAmount int64 TipAmount int64
ReceiptURL string ReceiptURL string
SquarePayID string // Square's payment ID SquarePayID string // Square's payment ID
Fees int64 // processing fee in pence Fees int64 // processing fee in pence
} }
type CheckoutResult struct { type CheckoutResult struct {
@@ -44,14 +44,14 @@ type CheckoutResult struct {
} }
type CardOnFile struct { type CardOnFile struct {
ID string ID string
CardID string // Square's card-on-file token CardID string // Square's card-on-file token
Brand string Brand string
Last4 string Last4 string
ExpMonth int ExpMonth int
ExpYear int ExpYear int
Fingerprint string Fingerprint string
IsDefault bool IsDefault bool
} }
type RefundResult struct { type RefundResult struct {
+34
View File
@@ -155,3 +155,37 @@ func TestValidateEmail_ValidMailsAreSafe(t *testing.T) {
} }
} }
} }
func TestParseCursor_Valid(t *testing.T) {
tm, id, err := ParseCursor("2026-06-15T10:30:00Z|abc123def456")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tm.Year() != 2026 || tm.Month() != 6 || tm.Day() != 15 {
t.Errorf("unexpected time: %v", tm)
}
if id != "abc123def456" {
t.Errorf("expected id 'abc123def456', got %q", id)
}
}
func TestParseCursor_InvalidFormat(t *testing.T) {
_, _, err := ParseCursor("not-a-valid-cursor")
if err == nil {
t.Fatal("expected error for invalid cursor format, got nil")
}
}
func TestParseCursor_InvalidTimestamp(t *testing.T) {
_, _, err := ParseCursor("not-a-time|abc123def456")
if err == nil {
t.Fatal("expected error for invalid timestamp, got nil")
}
}
func TestParseCursor_EmptyCursor(t *testing.T) {
_, _, err := ParseCursor("")
if err == nil {
t.Fatal("expected error for empty cursor, got nil")
}
}
+15
View File
@@ -1,10 +1,12 @@
package validators package validators
import ( import (
"fmt"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"reflect" "reflect"
"regexp" "regexp"
"strings" "strings"
"time"
) )
var Validate *validator.Validate var Validate *validator.Validate
@@ -31,3 +33,16 @@ func IsValidID(id string) bool {
} }
return validIDRegex.MatchString(id) return validIDRegex.MatchString(id)
} }
// ParseCursor splits a "createdAt|id" cursor string into its components.
func ParseCursor(cursor string) (time.Time, string, error) {
parts := strings.SplitN(cursor, "|", 2)
if len(parts) != 2 {
return time.Time{}, "", fmt.Errorf("invalid cursor format")
}
t, err := time.Parse(time.RFC3339, parts[0])
if err != nil {
return time.Time{}, "", fmt.Errorf("invalid cursor created_at: %w", err)
}
return t, parts[1], nil
}