fix: Fixed paddle relink sub
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
// MigrateLegacyPlans re-keys the pre-spec-7 plan rows and MUST run before
|
||||
@@ -212,23 +213,40 @@ func Backfill(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Pass 6: instances claimed before linked_from_placeholder_id existed have no
|
||||
// alias, and Paddle's custom_data still names the placeholder they were
|
||||
// claimed from — so their next webhook resolves to nothing. The claim wrote an
|
||||
// audit entry naming both ids, which is the only surviving record of the
|
||||
// placeholder, so reconstruct the alias from it.
|
||||
if err := backfillPlaceholderAliases(ctx); err != nil {
|
||||
// Pass 6: instances whose identity was rewritten before previous_instance_ids
|
||||
// existed carry no trail, and Paddle's custom_data still names the id they
|
||||
// were rewritten FROM — so their next webhook resolves to nothing. Both
|
||||
// rewrites wrote an audit entry naming the old id, which is the only surviving
|
||||
// record of it, so reconstruct the trail from those.
|
||||
if err := backfillInstanceIDHistory(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// backfillPlaceholderAliases rebuilds linked_from_placeholder_id from the
|
||||
// instance.placeholder_linked audit entries. Idempotent by filtering on the
|
||||
// absence of the field.
|
||||
func backfillPlaceholderAliases(ctx context.Context) error {
|
||||
// backfillInstanceIDHistory rebuilds previous_instance_ids from the audit entries
|
||||
// the two identity rewrites leave behind: a placeholder claim
|
||||
// ("instance.placeholder_linked", detail "from placeholder <id>") and a relink
|
||||
// ("instance.relinked", detail "was <id>").
|
||||
//
|
||||
// $addToSet is what makes it idempotent, and it also means a chain of relinks
|
||||
// accumulates rather than the last one winning. Entries are walked NEWEST first,
|
||||
// matching on the current id or an already-recovered one: an instance relinked
|
||||
// A→B→C answers to neither A nor B by the time this runs, so the C entry has to
|
||||
// record B before the B entry has anything to attach A to.
|
||||
func backfillInstanceIDHistory(ctx context.Context) error {
|
||||
prefixes := map[string]string{
|
||||
"instance.placeholder_linked": "from placeholder ",
|
||||
"instance.relinked": "was ",
|
||||
}
|
||||
actions := make(bson.A, 0, len(prefixes))
|
||||
for action := range prefixes {
|
||||
actions = append(actions, action)
|
||||
}
|
||||
|
||||
cur, err := db.Admin("admin_audit").Find(ctx,
|
||||
bson.M{"action": "instance.placeholder_linked"})
|
||||
bson.M{"action": bson.M{"$in": actions}},
|
||||
options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -237,27 +255,29 @@ func backfillPlaceholderAliases(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
const prefix = "from placeholder "
|
||||
linked := 0
|
||||
recorded := 0
|
||||
for _, e := range entries {
|
||||
prefix := prefixes[e.Action]
|
||||
if e.Target == "" || !strings.HasPrefix(e.Detail, prefix) {
|
||||
continue
|
||||
}
|
||||
placeholderID := strings.TrimSpace(strings.TrimPrefix(e.Detail, prefix))
|
||||
if placeholderID == "" || placeholderID == e.Target {
|
||||
oldID := strings.TrimSpace(strings.TrimPrefix(e.Detail, prefix))
|
||||
if oldID == "" || oldID == e.Target {
|
||||
continue
|
||||
}
|
||||
res, err := db.Admin("admin_instances").UpdateOne(ctx,
|
||||
bson.M{"instance_id": e.Target,
|
||||
"linked_from_placeholder_id": bson.M{"$exists": false}},
|
||||
bson.M{"$set": bson.M{"linked_from_placeholder_id": placeholderID}})
|
||||
bson.M{"$or": bson.A{
|
||||
bson.M{"instance_id": e.Target},
|
||||
bson.M{"previous_instance_ids": e.Target},
|
||||
}},
|
||||
bson.M{"$addToSet": bson.M{"previous_instance_ids": oldID}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
linked += int(res.ModifiedCount)
|
||||
recorded += int(res.ModifiedCount)
|
||||
}
|
||||
if linked > 0 {
|
||||
log.Printf("backfill: recovered %d placeholder aliases from the audit log", linked)
|
||||
if recorded > 0 {
|
||||
log.Printf("backfill: recovered %d instance id rewrites from the audit log", recorded)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -147,13 +147,14 @@ type Instance struct {
|
||||
// checkout has something to attach custom_data to, before the customer has
|
||||
// pasted their install's real UUID. Cleared when the instance is linked.
|
||||
Placeholder bool `bson:"placeholder,omitempty" json:"placeholder,omitempty"`
|
||||
// LinkedFromPlaceholderID is the id this row carried while it was a
|
||||
// self-hosted placeholder, kept forever after the claim rewrote InstanceID to
|
||||
// the install's real UUID. Paddle's copy of custom_data still names the
|
||||
// placeholder on any subscription created before the claim (and on any webhook
|
||||
// delivered while the outbound patch was failing), so this is what lets a
|
||||
// later webhook resolve to the right instance instead of erroring as unknown.
|
||||
LinkedFromPlaceholderID string `bson:"linked_from_placeholder_id,omitempty" json:"-"`
|
||||
// PreviousInstanceIDs is every id this row has carried before its current one.
|
||||
// A self-hosted row's identity is rewritten twice over its life — once when a
|
||||
// paid placeholder is claimed, and again on each relink to a rebuilt server —
|
||||
// and Paddle keeps its own copy of custom_data written at checkout. That copy
|
||||
// is patched on each rewrite, but the patch is best-effort and any event
|
||||
// already in flight still names an old id, so this is what lets a webhook
|
||||
// resolve to the right instance instead of erroring as unknown.
|
||||
PreviousInstanceIDs []string `bson:"previous_instance_ids,omitempty" json:"-"`
|
||||
// PendingOwnerUserID is the customer_user who bought a paid-cloud placeholder,
|
||||
// remembered so the confirmed-payment webhook can provision the instance with
|
||||
// them as owner. Cleared once provisioned. Only ever set on a cloud placeholder.
|
||||
|
||||
Reference in New Issue
Block a user