package service import ( "context" "github.com/calendarapi/internal/models" "github.com/calendarapi/internal/repository" "github.com/calendarapi/internal/utils" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) type ContactService struct { queries *repository.Queries audit *AuditService } func NewContactService(queries *repository.Queries, audit *AuditService) *ContactService { return &ContactService{queries: queries, audit: audit} } func (s *ContactService) Create(ctx context.Context, userID uuid.UUID, req CreateContactRequest) (*models.Contact, error) { if req.FirstName == nil && req.LastName == nil && req.Email == nil && req.Phone == nil { return nil, models.NewValidationError("at least one identifying field required") } if req.Email != nil { if err := utils.ValidateEmail(*req.Email); err != nil { return nil, err } } id := uuid.New() c, err := s.queries.CreateContact(ctx, repository.CreateContactParams{ ID: utils.ToPgUUID(id), OwnerID: utils.ToPgUUID(userID), FirstName: utils.ToPgTextPtr(req.FirstName), LastName: utils.ToPgTextPtr(req.LastName), Email: utils.ToPgTextPtr(req.Email), Phone: utils.ToPgTextPtr(req.Phone), Company: utils.ToPgTextPtr(req.Company), Notes: utils.ToPgTextPtr(req.Notes), }) if err != nil { return nil, models.ErrInternal } s.audit.Log(ctx, "contact", id, "CREATE_CONTACT", userID) return contactFromDB(c), nil } func (s *ContactService) Get(ctx context.Context, userID uuid.UUID, contactID uuid.UUID) (*models.Contact, error) { c, err := s.queries.GetContactByID(ctx, repository.GetContactByIDParams{ ID: utils.ToPgUUID(contactID), OwnerID: utils.ToPgUUID(userID), }) if err != nil { if err == pgx.ErrNoRows { return nil, models.ErrNotFound } return nil, models.ErrInternal } return contactFromDB(c), nil } func (s *ContactService) List(ctx context.Context, userID uuid.UUID, search *string, limit int, cursor string) ([]models.Contact, *string, error) { lim := utils.ClampLimit(limit) var cursorTime, cursorID interface{} p := repository.ListContactsParams{ OwnerID: utils.ToPgUUID(userID), Search: utils.ToPgTextPtr(search), Lim: lim + 1, } if cursor != "" { ct, cid, err := utils.ParseCursor(cursor) if err != nil { return nil, nil, models.NewValidationError("invalid cursor") } p.CursorTime = utils.ToPgTimestamptzPtr(ct) p.CursorID = utils.ToPgUUID(*cid) cursorTime = ct cursorID = cid } _ = cursorTime _ = cursorID rows, err := s.queries.ListContacts(ctx, p) if err != nil { return nil, nil, models.ErrInternal } contacts := make([]models.Contact, 0, len(rows)) for _, r := range rows { contacts = append(contacts, *contactFromDB(r)) } if int32(len(contacts)) > lim { contacts = contacts[:lim] last := contacts[len(contacts)-1] c := utils.EncodeCursor(last.CreatedAt, last.ID) return contacts, &c, nil } return contacts, nil, nil } func (s *ContactService) Update(ctx context.Context, userID uuid.UUID, contactID uuid.UUID, req UpdateContactRequest) (*models.Contact, error) { c, err := s.queries.UpdateContact(ctx, repository.UpdateContactParams{ ID: utils.ToPgUUID(contactID), OwnerID: utils.ToPgUUID(userID), FirstName: utils.ToPgTextPtr(req.FirstName), LastName: utils.ToPgTextPtr(req.LastName), Email: utils.ToPgTextPtr(req.Email), Phone: utils.ToPgTextPtr(req.Phone), Company: utils.ToPgTextPtr(req.Company), Notes: utils.ToPgTextPtr(req.Notes), }) if err != nil { if err == pgx.ErrNoRows { return nil, models.ErrNotFound } return nil, models.ErrInternal } s.audit.Log(ctx, "contact", contactID, "UPDATE_CONTACT", userID) return contactFromDB(c), nil } func (s *ContactService) Delete(ctx context.Context, userID uuid.UUID, contactID uuid.UUID) error { err := s.queries.SoftDeleteContact(ctx, repository.SoftDeleteContactParams{ ID: utils.ToPgUUID(contactID), OwnerID: utils.ToPgUUID(userID), }) if err != nil { return models.ErrInternal } s.audit.Log(ctx, "contact", contactID, "DELETE_CONTACT", userID) return nil } func contactFromDB(v repository.Contact) *models.Contact { return &models.Contact{ ID: utils.FromPgUUID(v.ID), FirstName: utils.FromPgText(v.FirstName), LastName: utils.FromPgText(v.LastName), Email: utils.FromPgText(v.Email), Phone: utils.FromPgText(v.Phone), Company: utils.FromPgText(v.Company), Notes: utils.FromPgText(v.Notes), CreatedAt: utils.FromPgTimestamptz(v.CreatedAt), UpdatedAt: utils.FromPgTimestamptz(v.UpdatedAt), } } type CreateContactRequest struct { FirstName *string LastName *string Email *string Phone *string Company *string Notes *string } type UpdateContactRequest struct { FirstName *string LastName *string Email *string Phone *string Company *string Notes *string }