Fix Square card linkage: reference_id instead of customer_id (no Square customer provisioning)

The app does not provision Square customers, so sending the local user ID as
customer_id in Create Card was rejected with CUSTOMER_NOT_FOUND, and filtering
List Cards by it returned nothing. reference_id is Square's free-form client
reference — max 128 chars, no uniqueness constraint — and is echoed in both
Create and List responses.

- Create Card payload: reference_id = local user ID (customer_id absent)
- List Cards: native ?reference_id=<userID> filter (no limit/customer_id,
  no client-side filter, no cursor handling needed)
- Mock parity: CreateCardOnFile stores ReferenceID; GetCardsOnFile unchanged
- Regression guards: TestCreateCardOnFileHTTP_IdempotencyKey asserts
  reference_id=user_1 and customer_id ABSENT; new
  TestGetCardsOnFileHTTP_ReferenceIDFilter asserts the query shape
This commit is contained in:
2026-08-22 00:34:49 +01:00
parent 53ca89603d
commit 16240d67e3
5 changed files with 80 additions and 14 deletions
+19 -8
View File
@@ -168,6 +168,7 @@ type sqCard struct {
CardholderName string `json:"cardholder_name,omitempty"`
Fingerprint string `json:"fingerprint"`
CustomerID string `json:"customer_id,omitempty"`
ReferenceID string `json:"reference_id,omitempty"`
Enabled bool `json:"enabled"`
Version int64 `json:"version"`
CreatedAt string `json:"created_at"`
@@ -259,6 +260,7 @@ type sqCardPayload struct {
ExpYear *int `json:"exp_year,omitempty"`
CardholderName string `json:"cardholder_name,omitempty"`
CustomerID string `json:"customer_id,omitempty"`
ReferenceID string `json:"reference_id,omitempty"`
}
type sqCreateCardResponse struct {
@@ -445,7 +447,11 @@ func createCardOnFileHTTPWithClient(ctx context.Context, userID, cardToken strin
IdempotencyKey: fmt.Sprintf("create-card-%x", ikHash),
SourceID: cardToken,
Card: sqCardPayload{
CustomerID: userID,
// The app does not provision Square customers, so the local user
// ID must NOT be sent as customer_id (Square would reject it).
// reference_id is Square's free-form client reference, used to link
// the card to the local user for client-side filtering.
ReferenceID: userID,
},
}
var resp sqCreateCardResponse
@@ -456,9 +462,17 @@ func createCardOnFileHTTPWithClient(ctx context.Context, userID, cardToken strin
}
func getCardsOnFileHTTP(ctx context.Context, userID string) ([]CardOnFile, error) {
hc := newHTTPClient()
return getCardsOnFileHTTPWithClient(ctx, userID, newHTTPClient())
}
func getCardsOnFileHTTPWithClient(ctx context.Context, userID string, hc *httpClient) ([]CardOnFile, error) {
// Filter by reference_id natively: Square's List Cards API supports the
// reference_id query param, and cards are created with reference_id = the
// local user ID (the app has no Square customers, so customer_id cannot be
// used). This avoids both the invalid customer_id filter and a client-side
// filter across a cursor-paginated list.
var resp sqListCardsResponse
if err := hc.doJSON(ctx, http.MethodGet, "/v2/cards?customer_id="+url.QueryEscape(userID), nil, &resp); err != nil {
if err := hc.doJSON(ctx, http.MethodGet, "/v2/cards?reference_id="+url.QueryEscape(userID), nil, &resp); err != nil {
return nil, err
}
cards := make([]CardOnFile, 0, len(resp.Cards))
@@ -552,10 +566,6 @@ func refundFromSquare(sq *sqRefund) *RefundResult {
}
func cardFromSquare(sq *sqCard, userID string) *CardOnFile {
customerID := sq.CustomerID
if customerID == "" {
customerID = userID
}
return &CardOnFile{
ID: sq.ID,
CardID: sq.ID,
@@ -565,7 +575,8 @@ func cardFromSquare(sq *sqCard, userID string) *CardOnFile {
ExpYear: sq.ExpYear,
Fingerprint: sq.Fingerprint,
CardholderName: sq.CardholderName,
CustomerID: customerID,
CustomerID: sq.CustomerID,
ReferenceID: sq.ReferenceID,
Enabled: sq.Enabled,
Version: sq.Version,
CreatedAt: sq.CreatedAt,