11package api
22
33import (
4+ "context"
45 "errors"
56 "log/slog"
67 "net/http"
7- "context "
8+ "strings "
89
910 "github.com/diggerhq/digger/opentaco/internal/domain"
1011 "github.com/diggerhq/digger/opentaco/internal/rbac"
@@ -29,17 +30,19 @@ func NewOrgHandler(orgRepo domain.OrganizationRepository, userRepo domain.UserRe
2930
3031// CreateOrgRequest is the request body for creating an organization
3132type CreateOrgRequest struct {
32- Name string `json:"name" validate:"required"` // Unique identifier (e.g., "acme")
33- DisplayName string `json:"display_name" validate:"required"` // Friendly name (e.g., "Acme Corp")
33+ Name string `json:"name" validate:"required"` // Unique identifier (e.g., "acme")
34+ DisplayName string `json:"display_name" validate:"required"` // Friendly name (e.g., "Acme Corp")
35+ ExternalOrgID string `json:"external_org_id"` // External org identifier (optional)
3436}
3537
3638// CreateOrgResponse is the response for creating an organization
3739type CreateOrgResponse struct {
38- ID string `json:"id"` // UUID
39- Name string `json:"name"` // Unique identifier
40- DisplayName string `json:"display_name"` // Friendly name
41- CreatedBy string `json:"created_by"`
42- CreatedAt string `json:"created_at"`
40+ ID string `json:"id"` // UUID
41+ Name string `json:"name"` // Unique identifier
42+ DisplayName string `json:"display_name"` // Friendly name
43+ ExternalOrgID string `json:"external_org_id"` // External org identifier
44+ CreatedBy string `json:"created_by"`
45+ CreatedAt string `json:"created_at"`
4346}
4447
4548// CreateOrganization handles POST /internal/orgs
@@ -102,7 +105,7 @@ func (h *OrgHandler) CreateOrganization(c echo.Context) error {
102105
103106 // Create organization in transaction
104107 err := h .orgRepo .WithTransaction (ctx , func (ctx context.Context , txRepo domain.OrganizationRepository ) error {
105- createdOrg , err := txRepo .Create (ctx , req .Name , req .DisplayName , userIDStr )
108+ createdOrg , err := txRepo .Create (ctx , req .Name , req .Name , req . DisplayName , req . ExternalOrgID , userIDStr )
106109 if err != nil {
107110 return err
108111 }
@@ -122,9 +125,17 @@ func (h *OrgHandler) CreateOrganization(c echo.Context) error {
122125 "error" : err .Error (),
123126 })
124127 }
128+
129+ // Check for external org ID conflict
130+ if strings .Contains (err .Error (), "external org ID already exists" ) {
131+ return c .JSON (http .StatusConflict , map [string ]string {
132+ "error" : err .Error (),
133+ })
134+ }
125135
126136 slog .Error ("Failed to create organization" ,
127137 "name" , req .Name ,
138+ "externalOrgID" , req .ExternalOrgID ,
128139 "error" , err ,
129140 )
130141 return c .JSON (http .StatusInternalServerError , map [string ]string {
@@ -162,11 +173,148 @@ func (h *OrgHandler) CreateOrganization(c echo.Context) error {
162173
163174 // Success - org created (and RBAC initialized if available)
164175 return c .JSON (http .StatusCreated , CreateOrgResponse {
165- ID : org .ID ,
166- Name : org .Name ,
167- DisplayName : org .DisplayName ,
168- CreatedBy : org .CreatedBy ,
169- CreatedAt : org .CreatedAt .Format ("2006-01-02T15:04:05Z07:00" ),
176+ ID : org .ID ,
177+ Name : org .Name ,
178+ DisplayName : org .DisplayName ,
179+ ExternalOrgID : org .ExternalOrgID ,
180+ CreatedBy : org .CreatedBy ,
181+ CreatedAt : org .CreatedAt .Format ("2006-01-02T15:04:05Z07:00" ),
182+ })
183+ }
184+
185+ // SyncExternalOrgRequest is the request body for syncing an external organization
186+ type SyncExternalOrgRequest struct {
187+ Name string `json:"name" validate:"required"` // Internal name (e.g., "acme")
188+ DisplayName string `json:"display_name" validate:"required"` // Friendly name (e.g., "Acme Corp")
189+ ExternalOrgID string `json:"external_org_id" validate:"required"` // External org identifier
190+ }
191+
192+ // SyncExternalOrgResponse is the response for syncing an external organization
193+ type SyncExternalOrgResponse struct {
194+ Status string `json:"status"` // "created" or "existing"
195+ Organization * domain.Organization `json:"organization"`
196+ }
197+
198+ // SyncExternalOrg handles POST /internal/orgs/sync
199+ // Creates a new organization with external mapping or returns existing one
200+ func (h * OrgHandler ) SyncExternalOrg (c echo.Context ) error {
201+ ctx := c .Request ().Context ()
202+
203+ // Get user context from webhook middleware
204+ userID := c .Get ("user_id" )
205+ email := c .Get ("email" )
206+
207+ if userID == nil || email == nil {
208+ slog .Error ("Missing user context in sync org request" )
209+ return c .JSON (http .StatusBadRequest , map [string ]string {
210+ "error" : "user context required" ,
211+ })
212+ }
213+
214+ userIDStr , ok := userID .(string )
215+ if ! ok || userIDStr == "" {
216+ slog .Error ("Invalid user_id type in context" )
217+ return c .JSON (http .StatusInternalServerError , map [string ]string {
218+ "error" : "invalid user context - webhook middleware misconfigured" ,
219+ })
220+ }
221+
222+ // Parse request
223+ var req SyncExternalOrgRequest
224+ if err := c .Bind (& req ); err != nil {
225+ slog .Error ("Failed to bind sync org request" , "error" , err )
226+ return c .JSON (http .StatusBadRequest , map [string ]string {
227+ "error" : "invalid request body" ,
228+ })
229+ }
230+
231+ if req .Name == "" || req .DisplayName == "" || req .ExternalOrgID == "" {
232+ return c .JSON (http .StatusBadRequest , map [string ]string {
233+ "error" : "name, display_name, and external_org_id are required" ,
234+ })
235+ }
236+
237+ slog .Info ("Syncing external organization" ,
238+ "name" , req .Name ,
239+ "displayName" , req .DisplayName ,
240+ "externalOrgID" , req .ExternalOrgID ,
241+ "createdBy" , userIDStr ,
242+ )
243+
244+ // Check if external org ID already exists
245+ existingOrg , err := h .orgRepo .GetByExternalID (ctx , req .ExternalOrgID )
246+ if err == nil {
247+ // External org ID exists, return existing org
248+ slog .Info ("External organization already exists" ,
249+ "externalOrgID" , req .ExternalOrgID ,
250+ "orgID" , existingOrg .ID ,
251+ )
252+ return c .JSON (http .StatusOK , SyncExternalOrgResponse {
253+ Status : "existing" ,
254+ Organization : existingOrg ,
255+ })
256+ }
257+
258+ if err != domain .ErrOrgNotFound {
259+ slog .Error ("Failed to check existing external org ID" ,
260+ "externalOrgID" , req .ExternalOrgID ,
261+ "error" , err ,
262+ )
263+ return c .JSON (http .StatusInternalServerError , map [string ]string {
264+ "error" : "failed to check existing external organization" ,
265+ })
266+ }
267+
268+ // Create new organization with external mapping
269+ var org * domain.Organization
270+ err = h .orgRepo .WithTransaction (ctx , func (ctx context.Context , txRepo domain.OrganizationRepository ) error {
271+ createdOrg , err := txRepo .Create (ctx , req .Name , req .Name , req .DisplayName , req .ExternalOrgID , userIDStr )
272+ if err != nil {
273+ return err
274+ }
275+ org = createdOrg
276+ return nil
277+ })
278+
279+ if err != nil {
280+ if errors .Is (err , domain .ErrOrgExists ) {
281+ return c .JSON (http .StatusConflict , map [string ]string {
282+ "error" : "organization name already exists" ,
283+ })
284+ }
285+ if errors .Is (err , domain .ErrInvalidOrgID ) {
286+ return c .JSON (http .StatusBadRequest , map [string ]string {
287+ "error" : err .Error (),
288+ })
289+ }
290+
291+ // Check for external org ID conflict
292+ if strings .Contains (err .Error (), "external org ID already exists" ) {
293+ return c .JSON (http .StatusConflict , map [string ]string {
294+ "error" : err .Error (),
295+ })
296+ }
297+
298+ slog .Error ("Failed to create organization during sync" ,
299+ "name" , req .Name ,
300+ "externalOrgID" , req .ExternalOrgID ,
301+ "error" , err ,
302+ )
303+ return c .JSON (http .StatusInternalServerError , map [string ]string {
304+ "error" : "failed to create organization" ,
305+ "detail" : err .Error (),
306+ })
307+ }
308+
309+ slog .Info ("External organization synced successfully" ,
310+ "name" , req .Name ,
311+ "externalOrgID" , req .ExternalOrgID ,
312+ "orgID" , org .ID ,
313+ )
314+
315+ return c .JSON (http .StatusCreated , SyncExternalOrgResponse {
316+ Status : "created" ,
317+ Organization : org ,
170318 })
171319}
172320
0 commit comments