Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions internal/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -722,12 +722,15 @@ func (s *Store) GetSubscription(userID string) (*models.Subscription, error) {
&sub.CreatedAt, &sub.UpdatedAt,
)
if err == sql.ErrNoRows {
// No subscription row: return a default free subscription
// No subscription row: return a default free subscription.
// AutoRenews is true rather than the zero value so the payload does
// not describe a free plan as something that will not renew.
return &models.Subscription{
UserID: userID,
Plan: "free",
Status: "active",
Currency: "usd",
UserID: userID,
Plan: "free",
Status: "active",
Currency: "usd",
AutoRenews: true,
}, nil
}
if err != nil {
Expand Down Expand Up @@ -925,6 +928,10 @@ func (s *Store) ListPaystackSubscriptionsNeedingRepair() ([]*models.Subscription
FROM subscriptions
WHERE provider = 'paystack'
AND COALESCE(provider_sub_id,'') NOT LIKE 'SUB\_%' ESCAPE '\'
-- Already reconciled. A row marked non-recurring will never gain a
-- SUB_ code, because there is no subscription to find — without this
-- it is reported as outstanding on every run, for ever.
AND COALESCE(auto_renews,1) = 1
ORDER BY created_at`)
if err != nil {
return nil, err
Expand Down
40 changes: 39 additions & 1 deletion internal/store/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,10 @@ func seedSub(t *testing.T, s *Store, email, provider, subID string) *models.Subs
Status: "active",
Currency: "ngn",
Interval: "month",
CreatedAt: time.Now().UTC(),
// Explicit: the zero value is false, which would mean "does not
// renew" and quietly exclude the row from the repair selection.
AutoRenews: true,
CreatedAt: time.Now().UTC(),
}
if err := s.UpsertSubscription(sub); err != nil {
t.Fatalf("seed subscription: %v", err)
Expand Down Expand Up @@ -363,3 +366,38 @@ func TestRepairPaystackSubscription(t *testing.T) {
t.Errorf("%d rows still need repair after repairing them all", len(rows))
}
}

// A row already marked non-recurring has been reconciled. It will never gain a
// SUB_ code, so leaving it selected means the repair reports outstanding work
// for ever and the "nothing to do" signal becomes meaningless.
func TestListPaystackSubscriptionsSkipsReconciledRows(t *testing.T) {
s := newTestStore(t)

sub := seedSub(t, s, "transfer@example.com", "paystack", "T981099203072787")

rows, err := s.ListPaystackSubscriptionsNeedingRepair()
if err != nil {
t.Fatalf("list: %v", err)
}
if len(rows) != 1 {
t.Fatalf("selected %d rows before repair, want 1", len(rows))
}

if err := s.MarkSubscriptionNonRecurring(sub.ID); err != nil {
t.Fatalf("mark: %v", err)
}

rows, err = s.ListPaystackSubscriptionsNeedingRepair()
if err != nil {
t.Fatalf("list after repair: %v", err)
}
if len(rows) != 0 {
t.Errorf("still selected after being marked non-recurring: %d rows", len(rows))
}

// The flag itself must have stuck.
got, _ := s.GetSubscription(sub.UserID)
if got.AutoRenews {
t.Error("auto_renews reverted to true")
}
}
Loading