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