~netlandish/links-dev

links: admin/billing: fixing payment history calls with a properly formatted response object. v1 APPLIED

Peter Sanchez: 1
 admin/billing: fixing payment history calls with a properly formatted response object.

 6 files changed, 110 insertions(+), 37 deletions(-)
Export patchset (mbox)
How do I use this?

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 -3
Learn more about email & git

[PATCH links] admin/billing: fixing payment history calls with a properly formatted response object. Export this patch

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