fix: Fixed paddle relink sub
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -13,6 +11,7 @@ import (
|
||||
"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/billing"
|
||||
"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/catalogue"
|
||||
"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/db"
|
||||
"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/licensing"
|
||||
"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/models"
|
||||
"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/paddle"
|
||||
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/license"
|
||||
@@ -247,32 +246,26 @@ func claimPlaceholderLink(c *gin.Context) {
|
||||
placeholderID := inst.InstanceID
|
||||
if _, err := db.Admin("admin_instances").UpdateOne(ctx,
|
||||
bson.M{"instance_id": placeholderID},
|
||||
bson.M{"$set": bson.M{
|
||||
"instance_id": body.InstanceID,
|
||||
"status": models.StatusActive,
|
||||
"placeholder": false,
|
||||
"linked_from_placeholder_id": placeholderID,
|
||||
}}); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
// Re-point the subscription from the placeholder id to the real UUID so
|
||||
// billing.IssueForInstance (and every later webhook) finds it.
|
||||
if _, err := db.Admin("subscriptions").UpdateMany(ctx,
|
||||
bson.M{"instance_id": placeholderID},
|
||||
bson.M{"$set": bson.M{"instance_id": body.InstanceID}}); err != nil {
|
||||
bson.M{
|
||||
"$set": bson.M{
|
||||
"instance_id": body.InstanceID,
|
||||
"status": models.StatusActive,
|
||||
"placeholder": false,
|
||||
},
|
||||
"$addToSet": bson.M{"previous_instance_ids": placeholderID},
|
||||
}); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Paddle holds its own copy of custom_data, written at checkout, and it still
|
||||
// names the placeholder. Every later event on this subscription — renewal,
|
||||
// cancellation, an entitlement change — is decoded from it, so leaving it
|
||||
// stale means the next webhook resolves to an id no row carries any more.
|
||||
// Best-effort: the linked_from_placeholder_id alias above is what makes the
|
||||
// webhook path correct whether or not this call lands, and the customer must
|
||||
// not be blocked from linking by an outbound API failure.
|
||||
syncSubscriptionCustomData(ctx, placeholderID, body.InstanceID, inst.AccountID)
|
||||
// Re-point the subscription rows from the placeholder id to the real UUID so
|
||||
// billing.IssueForInstance finds it, and rewrite Paddle's own copy of
|
||||
// custom_data — written at checkout, it still names the placeholder, and every
|
||||
// later event on this subscription is decoded from it.
|
||||
if err := licensing.RepointSubscriptions(ctx, placeholderID, body.InstanceID, inst.AccountID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := billing.IssueForInstance(ctx, body.InstanceID); err != nil {
|
||||
// The link stuck; issuance did not. The reconciler and a retry recover it,
|
||||
@@ -289,37 +282,6 @@ func claimPlaceholderLink(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"instance_id": body.InstanceID})
|
||||
}
|
||||
|
||||
// syncSubscriptionCustomData rewrites custom_data.instance_id at Paddle for
|
||||
// every subscription that named the placeholder, so future events decode to the
|
||||
// real UUID. Paddle replaces the whole object on a PATCH, so account_id is sent
|
||||
// alongside rather than dropped.
|
||||
//
|
||||
// Deliberately silent on failure: it is a convergence step, not the correctness
|
||||
// boundary — handleSubscription resolves a stale id through the instance row's
|
||||
// linked_from_placeholder_id either way.
|
||||
func syncSubscriptionCustomData(ctx context.Context, placeholderID, realID, accountID string) {
|
||||
cur, err := db.Admin("subscriptions").Find(ctx, bson.M{"instance_id": realID})
|
||||
if err != nil {
|
||||
log.Printf("claim link %s: read subscriptions: %v", realID, err)
|
||||
return
|
||||
}
|
||||
var subs []models.Subscription
|
||||
if err := cur.All(ctx, &subs); err != nil {
|
||||
log.Printf("claim link %s: decode subscriptions: %v", realID, err)
|
||||
return
|
||||
}
|
||||
for _, s := range subs {
|
||||
if s.PaddleSubscriptionID == "" {
|
||||
continue
|
||||
}
|
||||
if err := paddle.Get().UpdateSubscriptionCustomData(ctx, s.PaddleSubscriptionID,
|
||||
map[string]string{"account_id": accountID, "instance_id": realID}); err != nil {
|
||||
log.Printf("claim link %s: patch custom_data on %s (was %s): %v",
|
||||
realID, s.PaddleSubscriptionID, placeholderID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// billingPortal mints a Paddle customer-portal URL. The account must already
|
||||
// have a paddle_customer_id, which it learns from its first subscription webhook.
|
||||
func billingPortal(c *gin.Context) {
|
||||
|
||||
Reference in New Issue
Block a user