package invitationimp import ( "context" "encoding/json" "errors" "net/http" "github.com/tech/sendico/pkg/api/http/response" "github.com/tech/sendico/pkg/merrors" an "github.com/tech/sendico/pkg/messaging/notifications/account" "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mutil/mzap" "github.com/tech/sendico/server/interface/api/srequest" "go.mongodb.org/mongo-driver/v2/bson" "go.uber.org/zap" ) func (a *InvitationAPI) doAccept(ctx context.Context, invitationRef bson.ObjectID, accData *model.AccountData) error { inv, err := a.getPendingInvitation(ctx, invitationRef) if err != nil { return err } org, err := a.getOrganization(ctx, inv.OrganizationRef, inv.Content.Email) if err != nil { return err } if _, err := a.fetchOrCreateAccount(ctx, org, inv, accData); err != nil { return err } if err := a.db.Accept(ctx, invitationRef); err != nil { a.Logger.Warn("Failed to accept invitation", zap.Error(err), mzap.ObjRef("invitation_ref", invitationRef)) return err } return nil } func (a *InvitationAPI) sendWelcomeEmail(account *model.Account, token string) error { if err := a.producer.SendMessage(an.AccountCreated(a.Name(), *account.GetID(), token)); err != nil { a.Logger.Warn("Failed to send account creation notification", zap.Error(err)) return err } return nil } func (a *InvitationAPI) getPendingInvitation(ctx context.Context, invitationRef bson.ObjectID) (*model.Invitation, error) { a.Logger.Debug("Fetching invitation", mzap.ObjRef("invitation_ref", invitationRef)) var inv model.Invitation if err := a.db.Unprotected().Get(ctx, invitationRef, &inv); err != nil { a.Logger.Warn("Failed to fetch invitation", zap.Error(err), mzap.ObjRef("invitation_ref", invitationRef)) return nil, err } if inv.Status != model.InvitationCreated { a.Logger.Warn("Invitation is not pending", mzap.StorableRef(&inv)) return nil, merrors.InvalidArgument("Invitation is not pending") } return &inv, nil } func (a *InvitationAPI) getOrganization(ctx context.Context, orgRef bson.ObjectID, email string) (*model.Organization, error) { a.Logger.Debug("Fetching organization", mzap.ObjRef("organization_ref", orgRef), zap.String("email", email)) var org model.Organization if err := a.odb.Unprotected().Get(ctx, orgRef, &org); err != nil { a.Logger.Warn("Failed to fetch organization when processing invitation", zap.Error(err), mzap.ObjRef("organization_ref", orgRef), zap.String("email", email)) return nil, err } return &org, nil } func (a *InvitationAPI) fetchOrCreateAccount(ctx context.Context, org *model.Organization, inv *model.Invitation, accData *model.AccountData) (*model.Account, error) { account, err := a.adb.GetByEmail(ctx, inv.Content.Email) if errors.Is(err, merrors.ErrNoData) { a.Logger.Debug("Account is not registered, creating", zap.String("email", inv.Content.Email)) if accData == nil { a.Logger.Warn("Account data missing for unregistered invitation acceptance", zap.String("email", inv.Content.Email), mzap.StorableRef(inv)) return nil, merrors.InvalidArgument("No account data provided for invitation acceptance") } account = accData.ToAccount() if err := a.accService.ValidateAccount(account); err != nil { a.Logger.Info("Account validation failed", zap.Error(err), zap.String("email", inv.Content.Email)) return nil, err } // creates account and joins organization token, err := a.accService.CreateAccount(ctx, org, account, inv.RoleRef) if err != nil { a.Logger.Warn("Failed to create account", zap.Error(err), zap.String("email", inv.Content.Email)) return nil, err } // Send welcome email if err = a.sendWelcomeEmail(account, token); err != nil { a.Logger.Warn("Failed to send welcome email for new account created via invitation", zap.Error(err), zap.String("email", inv.Content.Email)) return nil, err } return account, nil } else if err != nil { a.Logger.Warn("Failed to fetch account by email", zap.Error(err), zap.String("email", inv.Content.Email)) return nil, err } else { // If account already exists, then just join organization if err := a.accService.JoinOrganization(ctx, org, account, inv.RoleRef); err != nil { a.Logger.Warn("Failed to join organization", zap.Error(err), mzap.StorableRef(account), mzap.StorableRef(org)) return nil, err } } return account, nil } func (a *InvitationAPI) accept(r *http.Request) http.HandlerFunc { invitationRef, err := a.irh.GetRef(r) if err != nil { return a.respondBadReference(r, err) } var req srequest.AcceptInvitation if err := json.NewDecoder(r.Body).Decode(&req); err != nil { a.Logger.Warn("Failed to decode request body", zap.Error(err), mzap.ObjRef("invitation_ref", invitationRef)) return response.BadPayload(a.Logger, a.Name(), err) } if _, err := a.tf.CreateTransaction().Execute(r.Context(), func(ctx context.Context) (any, error) { return nil, a.doAccept(ctx, invitationRef, req.Account) }); err != nil { a.Logger.Warn("Failed to accept invitation", zap.Error(err), mzap.ObjRef("invitation_ref", invitationRef)) return response.Auto(a.Logger, a.Name(), err) } return response.Success(a.Logger) }