diff --git a/provision/instance.go b/provision/instance.go index 3dc6bde..19fa8f0 100644 --- a/provision/instance.go +++ b/provision/instance.go @@ -17,13 +17,35 @@ var ErrNameRejected = errors.New("organisation name rejected") const maxSlugAttempts = 50 -// CreateInstance inserts an instance under the first free slug derived from name. +// CreateInstance inserts an instance under the first free slug derived from name, +// with a freshly generated ID. +func CreateInstance(ctx context.Context, db *mongo.Database, name string) (*models.Instance, error) { + return CreateInstanceWithID(ctx, db, uuid.NewString(), name) +} + +// CreateInstanceWithID inserts an instance under the first free slug derived from +// name, using a caller-supplied instance ID. +// +// A caller-supplied ID exists for the paid-cloud flow: a placeholder row is +// created before payment and provisioning happens on the confirmed-payment +// webhook. Provisioning with the placeholder's own ID keeps the id stable, so +// the subscription's custom_data never points at a rewritten row and later +// webhooks still resolve it. If an instance with this ID already exists — a +// webhook retried after a partial provision — it is returned as-is rather than +// duplicated. // // The count-then-insert loop is racy on its own. It is safe only because // instances.slug carries a unique index: a lost race surfaces as a duplicate-key // error, which we treat as "that slug is taken" and retry. Do not remove the // duplicate-key branch, and do not remove the index. -func CreateInstance(ctx context.Context, db *mongo.Database, name string) (*models.Instance, error) { +func CreateInstanceWithID(ctx context.Context, db *mongo.Database, instanceID, name string) (*models.Instance, error) { + // Idempotency: a retried provision finds its own instance already present. + var existing models.Instance + if err := db.Collection("instances").FindOne(ctx, + bson.M{"instance_id": instanceID}).Decode(&existing); err == nil { + return &existing, nil + } + base, err := BaseSlug(name) if err != nil { return nil, fmt.Errorf("%w: %s", ErrNameRejected, err.Error()) @@ -41,7 +63,7 @@ func CreateInstance(ctx context.Context, db *mongo.Database, name string) (*mode } inst := models.Instance{ - InstanceID: uuid.NewString(), + InstanceID: instanceID, Name: name, Slug: slug, CreatedAt: time.Now().UTC(),