Peter Sanchez: 1 admin/billing: fixing payment history calls with a properly formatted response object. 6 files changed, 110 insertions(+), 37 deletions(-)
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.code.netlandish.com/~netlandish/links-dev/patches/231/mbox | git am -3Learn more about email & git
Changelog-fixed: bug in admin, and billing areas, calling payment history queries. --- admin/routes.go | 25 +--------- api/api_test.go | 77 +++++++++++++++++++++++++++++++ billing/routes.go | 13 +----- client.go | 28 +++++++++++ models/models.go | 2 +- templates/admin_billing_list.html | 2 +- 6 files changed, 110 insertions(+), 37 deletions(-) diff --git a/admin/routes.go b/admin/routes.go index a37d111..e36c97e 100644 --- a/admin/routes.go +++ b/admin/routes.go @@ -426,18 +426,7 @@ func (s *Service) OrgDetail(c echo.Context) error { return err } - type GraphQLHistoryResponse struct { - Payments struct { - Result []models.Invoice `json:"result"` - PageInfo struct { - Cursor string - HasNextPage bool - HasPrevPage bool - } `json:"pageInfo"` - } `json:"getPaymentHistory"` - } - - var historyResult GraphQLHistoryResponse + var historyResult links.PaymentHistoryResponse op = gqlclient.NewOperation( `query GetPaymentHistory($orgSlug: String, $after: Cursor, $before: Cursor) { getPaymentHistory(input: { @@ -976,17 +965,7 @@ func (s *Service) BillingList(c echo.Context) error { return err } - type GraphQLHistoryResponse struct { - Payments struct { - Result []models.Invoice `json:"result"` - PageInfo struct { - Cursor string - HasNextPage bool - HasPrevPage bool - } `json:"pageInfo"` - } `json:"getPaymentHistory"` - } - var historyResult GraphQLHistoryResponse + var historyResult links.PaymentHistoryResponse op = gqlclient.NewOperation( `query GetPaymentHistory($after: Cursor, $before: Cursor, $dStart: String, $dEnd: String, $interval: Int) { getPaymentHistory(input: { diff --git a/api/api_test.go b/api/api_test.go index ec779dd..f76d413 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -3816,4 +3816,81 @@ func TestAPI(t *testing.T) { c.Contains(err.Error(), "BaseURL Not Found") }) + t.Run("get payment history org fields", func(t *testing.T) { + // User 3 is the seeded superuser. orgId/orgSlug are only populated for + // superusers by the getPaymentHistory resolver. + superCtx := server.ServerContext(context.Background(), srv) + superCtx = auth.Context(superCtx, test.NewTestUser(3, true, true, true, true)) + superCtx = crypto.Context(superCtx, entropy) + + // models.Invoice.SubscriptionID is a plain int, so the row needs a real + // subscription to scan back out + var subID int + err := sq.Insert("subscriptions"). + Columns("user_id", "org_id", "stripe_id"). + Values(1, 2, "sub_paymenthistory"). + Suffix("RETURNING id"). + PlaceholderFormat(database.GetPlaceholderFormat()). + RunWith(srv.DB). + QueryRow(). + Scan(&subID) + c.NoError(err) + + insertInvoice := func(userID, orgID any, stripeID string, amount int) { + _, err := sq.Insert("invoices"). + Columns("status", "user_id", "org_id", "subscription_id", "stripe_id", + "currency", "amount", "amount_paid", "amount_net", "amount_refunded", + "payment_fee", "hosted_invoice_url"). + Values(models.InvoiceStatusPaid, userID, orgID, subID, stripeID, "USD", + amount, amount, amount-100, 0, 100, "https://stripe.test/"+stripeID). + PlaceholderFormat(database.GetPlaceholderFormat()). + RunWith(srv.DB). + Exec() + c.NoError(err) + } + insertInvoice(1, 2, "in_orgtest", 1000) + insertInvoice(nil, nil, "in_orphan", 500) + + var result links.PaymentHistoryResponse + op := gqlclient.NewOperation( + `query GetPaymentHistory($interval: Int) { + getPaymentHistory(input: {interval: $interval, filter: true}) { + result { + id + amount + amountRefunded + amountPaid + amountNet + paymentFee + orgId + orgSlug + hostedInvoiceURL + createdOn + } + pageInfo { + cursor + hasPrevPage + hasNextPage + } + } + }`) + op.Var("interval", links.FilterLast12Months) + err = links.Execute(superCtx, op, &result) + c.NoError(err) + c.Equal(2, len(result.Payments.Result)) + + // Ordered by id DESC, so the orphaned invoice comes first + orphan := result.Payments.Result[0] + c.Equal(0, orphan.OrgID) + c.Equal("", orphan.OrgSlug) + c.Equal(500, orphan.Amount) + + orgPayment := result.Payments.Result[1] + c.Equal(2, orgPayment.OrgID) + c.Equal("business_org", orgPayment.OrgSlug) + c.Equal(1000, orgPayment.Amount) + c.Equal(900, orgPayment.AmountNet) + c.Equal(100, orgPayment.PaymentFee) + }) + } diff --git a/billing/routes.go b/billing/routes.go index 87f71ea..82fd992 100644 --- a/billing/routes.go @@ -230,18 +230,7 @@ func (s *Service) SubscriptionHistory(c echo.Context) error { back = c.Echo().Reverse("core:org_list") } - type GraphQLResponse struct { - Payments struct { - Result []models.Invoice `json:"result"` - PageInfo struct { - Cursor string - HasNextPage bool - HasPrevPage bool - } `json:"pageInfo"` - } `json:"getPaymentHistory"` - } - - var result GraphQLResponse + var result links.PaymentHistoryResponse op := gqlclient.NewOperation( `query GetPaymentHistory($orgSlug: String, $after: Cursor, $before: Cursor) { getPaymentHistory(input: { diff --git a/client.go b/client.go index 8855cdf..95f5318 100644 --- a/client.go +++ b/client.go @@ -103,3 +103,31 @@ func Execute(ctx context.Context, op *gqlclient.Operation, result any) error { } return err } + +// Payment mirrors the GraphQL Payment type returned by getPaymentHistory. +// It is deliberately not models.Invoice: the API exposes the nullable org +// fields as plain scalars, which will not decode into sql.Null* types. +type Payment struct { + ID int `json:"id"` + Amount int `json:"amount"` + AmountRefunded int `json:"amountRefunded"` + AmountPaid int `json:"amountPaid"` + AmountNet int `json:"amountNet"` + PaymentFee int `json:"paymentFee"` + OrgID int `json:"orgId"` + OrgSlug string `json:"orgSlug"` + HostedInvoiceURL string `json:"hostedInvoiceURL"` + CreatedOn time.Time `json:"createdOn"` +} + +// PaymentHistoryResponse is a struct for getPaymentHistory gql query response storage +type PaymentHistoryResponse struct { + Payments struct { + Result []Payment `json:"result"` + PageInfo struct { + Cursor string + HasNextPage bool + HasPrevPage bool + } `json:"pageInfo"` + } `json:"getPaymentHistory"` +} diff --git a/models/models.go b/models/models.go index 0113e44..a350b07 100644 --- a/models/models.go +++ b/models/models.go @@ -428,7 +428,7 @@ type Invoice struct { Amount int `db:"amount"` AmountPaid int `db:"amount_paid"` AmountNet int `db:"amount_net"` - AmountRefunded int `db:"amount_net"` + AmountRefunded int `db:"amount_refunded"` PaymentFee int `db:"payment_fee"` HostedInvoiceURL string `db:"hosted_invoice_url"` CreatedOn time.Time `db:"created_on"` diff --git a/templates/admin_billing_list.html b/templates/admin_billing_list.html index 5b981f0..603ec12 100644 --- a/templates/admin_billing_list.html +++ b/templates/admin_billing_list.html @@ -109,7 +109,7 @@ <td>${{formatAmt .AmountPaid}}</td> <td>${{formatAmt .PaymentFee}}</td> <td>${{formatAmt .AmountNet}}</td> - <td><a href="{{reverse "admin:org_detail" .OrgSlug}}">{{.OrgSlug}}</a></td> + <td>{{if .OrgSlug}}<a href="{{reverse "admin:org_detail" .OrgSlug}}">{{.OrgSlug}}</a>{{end}}</td> <td>{{formatDate .CreatedOn}}</td> </tr> {{end}} -- 2.54.0
Applied. To git@git.code.netlandish.com:~netlandish/links fa02dbc..700e742 master -> master