Skip to content

Commit

Permalink
Add automated exports support (blacklightcms#133)
Browse files Browse the repository at this point in the history
  • Loading branch information
103124 authored and cristiangraz committed Oct 29, 2019
1 parent ddff062 commit d7f63bc
Show file tree
Hide file tree
Showing 8 changed files with 323 additions and 0 deletions.
79 changes: 79 additions & 0 deletions automated_exports.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package recurly

import (
"context"
"encoding/xml"
"fmt"
"net/http"
"time"
)

// AutomatedExportsService manages the interactions for automated exports.
type AutomatedExportsService interface {
// Get retrieves export file.
//
// https://dev.recurly.com/docs/download-export-file
Get(ctx context.Context, date time.Time, fileName string) (*AutomatedExport, error)

// ListDates returns a list of dates with export files.
//
// https://dev.recurly.com/v2.8/docs/list-export-dates
ListDates(opts *PagerOptions) Pager

// ListFiles returns a list of files available for the date specified.
//
// https://dev.recurly.com/v2.8/docs/list-export-files
ListFiles(date time.Time, opts *PagerOptions) Pager
}

// AutomatedExport holds export file info.
type AutomatedExport struct {
XMLName xml.Name `xml:"export_file"`
ExpiresAt NullTime `xml:"expires_at,omitempty"`
DownloadURL string `xml:"download_url,omitempty"`
}

// ExportDate holds export date info.
type ExportDate struct {
XMLName xml.Name `xml:"export_date"`
Date string `xml:"date,omitempty"`
}

// ExportFile holds export file info.
type ExportFile struct {
XMLName xml.Name `xml:"export_file"`
Name string `xml:"name,omitempty"`
}

var _ AutomatedExportsService = &automatedExportsImpl{}

// automatedExportsImpl implements AutomatedExportsService.
type automatedExportsImpl serviceImpl

func (s *automatedExportsImpl) Get(ctx context.Context, date time.Time, fileName string) (*AutomatedExport, error) {
d := date.Format("2006-01-02")
path := fmt.Sprintf("/export_dates/%s/export_files/%s", d, fileName)
req, err := s.client.newRequest("GET", path, nil)
if err != nil {
return nil, err
}

var dst AutomatedExport
if _, err := s.client.do(ctx, req, &dst); err != nil {
if e, ok := err.(*ClientError); ok && e.Response.StatusCode == http.StatusNotFound {
return nil, nil
}
return nil, err
}
return &dst, nil
}

func (s *automatedExportsImpl) ListDates(opts *PagerOptions) Pager {
return s.client.newPager("GET", "/export_dates", opts)
}

func (s *automatedExportsImpl) ListFiles(date time.Time, opts *PagerOptions) Pager {
d := date.Format("2006-01-02")
path := fmt.Sprintf("/export_dates/%s/export_files", d)
return s.client.newPager("GET", path, opts)
}
182 changes: 182 additions & 0 deletions automated_exports_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package recurly_test

import (
"bytes"
"context"
"encoding/xml"
"log"
"net/http"
"strconv"
"testing"
"time"

"github.com/blacklightcms/recurly"
"github.com/google/go-cmp/cmp"
)

// Ensure structs are encoded to XML properly.
func TestAutomatedExports_Encoding(t *testing.T) {
now := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
tests := []struct {
v interface{}
expected string
}{
{
v: recurly.AutomatedExport{ExpiresAt: recurly.NewTime(now), DownloadURL: "https://recurly.com/sub.csv.gz"},
expected: MustCompactString(`
<export_file>
<expires_at>2000-01-01T00:00:00Z</expires_at>
<download_url>https://recurly.com/sub.csv.gz</download_url>
</export_file>
`),
},
{
v: recurly.ExportDate{Date: "2019-10-10"},
expected: MustCompactString(`
<export_date>
<date>2019-10-10</date>
</export_date>
`),
},
{
v: recurly.ExportFile{Name: "account_notes_created.csv.gz"},
expected: MustCompactString(`
<export_file>
<name>account_notes_created.csv.gz</name>
</export_file>
`),
},
}

for i, tt := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
buf := new(bytes.Buffer)
if err := xml.NewEncoder(buf).Encode(tt.v); err != nil {
t.Fatal(err)
} else if buf.String() != tt.expected {
log.Print(tt.expected)
t.Fatal(buf.String())
}
})
}
}

func TestAutomatedExports_Get(t *testing.T) {
now := time.Date(2015, 2, 4, 23, 13, 7, 0, time.UTC)

t.Run("OK", func(t *testing.T) {
client, s := recurly.NewTestServer()
defer s.Close()

s.HandleFunc("GET", "/v2/export_dates/2015-02-04/export_files/sub.csv.gz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write(MustOpenFile("automated_export.xml"))
}, t)

if a, err := client.AutomatedExports.Get(context.Background(), now, "sub.csv.gz"); err != nil {
t.Fatal(err)
} else if diff := cmp.Diff(a, NewTestAutomatedExport()); diff != "" {
t.Fatal(diff)
} else if !s.Invoked {
t.Fatal("expected fn invocation")
}
})

// Ensure a 404 returns nil values.
t.Run("ErrNotFound", func(t *testing.T) {
client, s := recurly.NewTestServer()
defer s.Close()

s.HandleFunc("GET", "/v2/export_dates/2015-02-04/export_files/sub.csv.gz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}, t)

if a, err := client.AutomatedExports.Get(context.Background(), now, "sub.csv.gz"); !s.Invoked {
t.Fatal("expected fn invocation")
} else if err != nil {
t.Fatal(err)
} else if a != nil {
t.Fatalf("expected nil: %#v", a)
}
})
}

func TestAutomatedExports_ListDates(t *testing.T) {
client, s := recurly.NewTestServer()
defer s.Close()

var invocations int
s.HandleFunc("GET", "/v2/export_dates", func(w http.ResponseWriter, r *http.Request) {
invocations++
w.WriteHeader(http.StatusOK)
w.Write(MustOpenFile("export_dates.xml"))
}, t)

pager := client.AutomatedExports.ListDates(nil)
for pager.Next() {
var dates []recurly.ExportDate
if err := pager.Fetch(context.Background(), &dates); err != nil {
t.Fatal(err)
} else if !s.Invoked {
t.Fatal("expected s to be invoked")
} else if diff := cmp.Diff(dates, []recurly.ExportDate{*NewTestExportDate()}); diff != "" {
t.Fatal(diff)
}
}
if invocations != 1 {
t.Fatalf("unexpected number of invocations: %d", invocations)
}
}

func TestAutomatedExports_ListFiles(t *testing.T) {
now := time.Date(2019, 10, 10, 23, 13, 7, 0, time.UTC)
client, s := recurly.NewTestServer()
defer s.Close()

var invocations int
s.HandleFunc("GET", "/v2/export_dates/2019-10-10/export_files", func(w http.ResponseWriter, r *http.Request) {
invocations++
w.WriteHeader(http.StatusOK)
w.Write(MustOpenFile("export_files.xml"))
}, t)

pager := client.AutomatedExports.ListFiles(now, nil)
for pager.Next() {
var files []recurly.ExportFile
if err := pager.Fetch(context.Background(), &files); err != nil {
t.Fatal(err)
} else if !s.Invoked {
t.Fatal("expected s to be invoked")
} else if diff := cmp.Diff(files, []recurly.ExportFile{*NewTestExportFile()}); diff != "" {
t.Fatal(diff)
}
}
if invocations != 1 {
t.Fatalf("unexpected number of invocations: %d", invocations)
}
}

// Returns an AutomatedExport corresponding to testdata/adjustment.xml.
func NewTestAutomatedExport() *recurly.AutomatedExport {
return &recurly.AutomatedExport{
XMLName: xml.Name{Local: "export_file"},
ExpiresAt: recurly.NewTime(MustParseTime("2015-02-04T23:13:07Z")),
DownloadURL: "https://recurly.s3.amazonaws.com/file",
}
}

// Returns an ExportDate corresponding to testdata/export_dates.xml.
func NewTestExportDate() *recurly.ExportDate {
return &recurly.ExportDate{
XMLName: xml.Name{Local: "export_date"},
Date: "2019-10-10",
}
}

// Returns an ExportFile corresponding to testdata/export_files.xml.
func NewTestExportFile() *recurly.ExportFile {
return &recurly.ExportFile{
XMLName: xml.Name{Local: "export_file"},
Name: "account_notes_created.csv.gz",
}
}
37 changes: 37 additions & 0 deletions mock/automated_exports.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package mock

import (
"context"
"time"

"github.com/blacklightcms/recurly"
)

var _ recurly.AutomatedExportsService = &AutomatedExportsService{}

// AutomatedExportsService manages the interactions for automated exports.
type AutomatedExportsService struct {
OnGet func(ctx context.Context, date time.Time, fileName string) (*recurly.AutomatedExport, error)
GetInvoked bool

OnListDates func(opts *recurly.PagerOptions) recurly.Pager
ListDatesInvoked bool

OnListFiles func(date time.Time, opts *recurly.PagerOptions) recurly.Pager
ListFilesInvoked bool
}

func (m *AutomatedExportsService) Get(ctx context.Context, date time.Time, fileName string) (*recurly.AutomatedExport, error) {
m.GetInvoked = true
return m.OnGet(ctx, date, fileName)
}

func (m *AutomatedExportsService) ListDates(opts *recurly.PagerOptions) recurly.Pager {
m.ListDatesInvoked = true
return m.OnListDates(opts)
}

func (m *AutomatedExportsService) ListFiles(date time.Time, opts *recurly.PagerOptions) recurly.Pager {
m.ListFilesInvoked = true
return m.OnListFiles(date, opts)
}
6 changes: 6 additions & 0 deletions pager.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ func (p *pager) Fetch(ctx context.Context, dst interface{}) error {
AddOn []AddOn `xml:"add_on"`
Coupon []Coupon `xml:"coupon"`
CreditPayment []CreditPayment `xml:"credit_payment"`
ExportDate []ExportDate `xml:"export_date"`
ExportFile []ExportFile `xml:"export_file"`
Invoice []Invoice `xml:"invoice"`
Note []Note `xml:"note"`
Plan []Plan `xml:"plan"`
Expand Down Expand Up @@ -148,6 +150,10 @@ func (p *pager) Fetch(ctx context.Context, dst interface{}) error {
*v = unmarshaler.Coupon
case *[]CreditPayment:
*v = unmarshaler.CreditPayment
case *[]ExportDate:
*v = unmarshaler.ExportDate
case *[]ExportFile:
*v = unmarshaler.ExportFile
case *[]Invoice:
*v = unmarshaler.Invoice
case *[]Note:
Expand Down
2 changes: 2 additions & 0 deletions recurly.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type Client struct {
Accounts AccountsService
Adjustments AdjustmentsService
AddOns AddOnsService
AutomatedExports AutomatedExportsService
Billing BillingService
Coupons CouponsService
CreditPayments CreditPaymentsService
Expand Down Expand Up @@ -95,6 +96,7 @@ func NewClient(subdomain, apiKey string) *Client {
client.Accounts = &accountsImpl{client: client}
client.Adjustments = &adjustmentsImpl{client: client}
client.AddOns = &addOnsImpl{client: client}
client.AutomatedExports = &automatedExportsImpl{client: client}
client.Billing = &billingImpl{client: client}
client.Coupons = &couponsImpl{client: client}
client.CreditPayments = &creditInvoicesImpl{client: client}
Expand Down
5 changes: 5 additions & 0 deletions testdata/automated_export.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<export_file href="https://your-subbdomain.recurly.com/v2/export_dates/2019-10-10/export_files/sub.csv.gz">
<download_url>https://recurly.s3.amazonaws.com/file</download_url>
<expires_at type="datetime">2015-02-04T23:13:07Z</expires_at>
</export_file>
6 changes: 6 additions & 0 deletions testdata/export_dates.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<export_dates type="array">
<export_date>
<date>2019-10-10</date>
</export_date>
</export_dates>
6 changes: 6 additions & 0 deletions testdata/export_files.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<export_files type="array">
<export_file>
<name>account_notes_created.csv.gz</name>
</export_file>
</export_files>

0 comments on commit d7f63bc

Please sign in to comment.