Skip to content
This repository has been archived by the owner on Jan 2, 2024. It is now read-only.

Commit

Permalink
api: add /v1/payment_methods (#258)
Browse files Browse the repository at this point in the history
This adds support for listing payment methods for an org.

Fixes: TIE-118
  • Loading branch information
bmizerany committed Feb 21, 2023
1 parent 237d45b commit 5091a1d
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 0 deletions.
16 changes: 16 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ func (h *Handler) serve(w http.ResponseWriter, r *http.Request) error {
return h.servePull(w, r)
case "/v1/push":
return h.servePush(w, r)
case "/v1/payment_methods":
return h.servePaymentMethods(w, r)
default:
return trweb.NotFound
}
Expand Down Expand Up @@ -396,6 +398,20 @@ func (h *Handler) servePush(w http.ResponseWriter, r *http.Request) error {
return httpJSON(w, apitypes.PushResponse{Results: ee})
}

func (h *Handler) servePaymentMethods(w http.ResponseWriter, r *http.Request) error {
org := r.FormValue("org")

pms, err := h.c.LookupPaymentMethods(r.Context(), org)
if err != nil {
return err
}

return httpJSON(w, apitypes.PaymentMethodsResponse{
Org: org,
PaymentMethods: pms,
})
}

func httpJSON(w http.ResponseWriter, v any) error {
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
Expand Down
21 changes: 21 additions & 0 deletions api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,27 @@ func TestScheduleWithCustomerInfoNoPhases(t *testing.T) {
}
}

func TestPaymentMethods(t *testing.T) {
ctx := context.Background()
tc, _ := newTestClient(t)

if err := tc.Subscribe(ctx, "org:test"); err != nil {
t.Fatal(err)
}

got, err := tc.LookupPaymentMethods(ctx, "org:test")
if err != nil {
t.Fatal(err)
}

want := apitypes.PaymentMethodsResponse{
Org: "org:test",
PaymentMethods: nil,
}

diff.Test(t, t.Errorf, got, want)
}

func maybeFailNow(t *testing.T) {
t.Helper()
if t.Failed() {
Expand Down
6 changes: 6 additions & 0 deletions api/apitypes/apitypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"time"

"tier.run/refs"
"tier.run/types/payment"
)

type Error struct {
Expand All @@ -31,6 +32,11 @@ type PhaseResponse struct {
Fragments []refs.FeaturePlan `json:"fragments,omitempty"`
}

type PaymentMethodsResponse struct {
Org string `json:"org"`
PaymentMethods []payment.Method `json:"methods"`
}

type InvoiceSettings struct {
DefaultPaymentMethod string `json:"default_payment_method"`
}
Expand Down
4 changes: 4 additions & 0 deletions client/tier/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ func (c *Client) LookupLimits(ctx context.Context, org string) (apitypes.UsageRe
return fetchOK[apitypes.UsageResponse, *apitypes.Error](ctx, c, "GET", "/v1/limits?org="+org, nil)
}

func (c *Client) LookupPaymentMethods(ctx context.Context, org string) (apitypes.PaymentMethodsResponse, error) {
return fetchOK[apitypes.PaymentMethodsResponse, *apitypes.Error](ctx, c, "GET", "/v1/payment_methods?org="+org, nil)
}

// LookupLimit reports the current usage and limits for the provided org and
// feature. If the feature is not currently available to the org, both limit
// and used are zero and no error is reported.
Expand Down
11 changes: 11 additions & 0 deletions control/schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"kr.dev/errorfmt"
"tier.run/refs"
"tier.run/stripe"
"tier.run/types/payment"
"tier.run/values"
)

Expand Down Expand Up @@ -674,6 +675,16 @@ func (c *Client) LookupPhases(ctx context.Context, org string) (ps []Phase, err
return all, err
}

// LookupPaymentMethods returns the payment methods for the given org.
func (c *Client) LookupPaymentMethods(ctx context.Context, org string) ([]payment.Method, error) {
cid, err := c.WhoIs(ctx, org)
if err != nil {
return nil, err
}
var f stripe.Form
return stripe.Slurp[payment.Method](ctx, c.Stripe, "GET", "/v1/customers/"+cid+"/payment_methods", f)
}

type Period struct {
Effective time.Time
End time.Time
Expand Down
19 changes: 19 additions & 0 deletions control/schedule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,25 @@ func TestLookupPhases(t *testing.T) {
diff.Test(t, t.Errorf, got, want, ignoreProviderIDs)
}

func TestLookupPaymentMethods(t *testing.T) {
tc := newTestClient(t)
ctx := context.Background()
if err := tc.PutCustomer(ctx, "org:example", nil); err != nil {
t.Fatal(err)
}

// We can't get payment methods back from Stripe in Test Mode without
// manually creating them via SetupIntents. So we'll just check that we
// get an empty list, without an error.
pms, err := tc.LookupPaymentMethods(ctx, "org:example")
if err != nil {
t.Fatal(err)
}
if len(pms) != 0 {
t.Errorf("len(pms) = %d, expected 0", len(pms))
}
}

func TestCheckoutRequiredAddress(t *testing.T) {
type G struct {
successURL string
Expand Down
33 changes: 33 additions & 0 deletions types/payment/payment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package payment

import (
"encoding/json"
"time"
)

type Method struct {
raw json.RawMessage // hidden so we can add fields to Method later, and replace if neccessary
info struct {
ID string
Created int
}
}

func (m *Method) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &m.raw); err != nil {
return err
}
return json.Unmarshal(data, &m.info)
}

func (m Method) MarshalJSON() ([]byte, error) {
return m.raw.MarshalJSON()
}

func (m Method) Created() time.Time {
return time.Unix(int64(m.info.Created), 0)
}

func (m Method) ProviderID() string {
return m.info.ID
}
100 changes: 100 additions & 0 deletions types/payment/payment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package payment

import (
"encoding/json"
"testing"
"time"

"kr.dev/diff"
)

const testPaymentMethodJSON = `{
"id": "pm_1MdiJIIhUsqiXPckNAfPOQlt",
"object": "payment_method",
"billing_details": {
"address": {
"city": "San Francisco",
"country": "US",
"line1": "1234 Fake Street",
"line2": null,
"postal_code": "94102",
"state": "CA"
},
"email": "jenny@example.com",
"name": null,
"phone": "+15555555555"
},
"card": {
"brand": "visa",
"checks": {
"address_line1_check": null,
"address_postal_code_check": null,
"cvc_check": "pass"
},
"country": "US",
"exp_month": 8,
"exp_year": 2024,
"fingerprint": "noAKwq37KQONloUT",
"funding": "credit",
"generated_from": null,
"last4": "4242",
"networks": {
"available": [
"visa"
],
"preferred": null
},
"three_d_secure_usage": {
"supported": true
},
"wallet": null
},
"created": 123456789,
"customer": null,
"livemode": false,
"metadata": {
"order_id": "123456789"
},
"type": "card"
}`

func TestPayment(t *testing.T) {
var pm Method
err := json.Unmarshal([]byte(testPaymentMethodJSON), &pm)
if err != nil {
t.Fatal(err)
}

if !pm.Created().Equal(time.Unix(123456789, 0)) {
t.Fatalf("Created = %v; want 123456789", pm.Created().Unix())
}

if pm.ProviderID() != "pm_1MdiJIIhUsqiXPckNAfPOQlt" {
t.Fatalf("ID = %q; want pm_1MdiJIIhUsqiXPckNAfPOQlt", pm.ProviderID())
}
}

func TestMethodRoundTrip(t *testing.T) {
var pm Method
err := json.Unmarshal([]byte(testPaymentMethodJSON), &pm)
if err != nil {
t.Fatal(err)
}

var ma map[string]any
if err := json.Unmarshal([]byte(testPaymentMethodJSON), &ma); err != nil {
t.Fatal(err)
}

data, err := json.Marshal(pm)
if err != nil {
t.Fatal(err)
}

var mb map[string]any
if err = json.Unmarshal(data, &mb); err != nil {
t.Fatal(err)
}

diff.Test(t, t.Errorf, ma, mb)
}

0 comments on commit 5091a1d

Please sign in to comment.