@@ -74,6 +74,17 @@ type V1OrganizationResponse struct {
7474 Domain string `json:"Domains"`
7575}
7676
77+ // V1OrganizationListResponse represents the list response from GET /v1/orgs/search
78+ type V1OrganizationListResponse struct {
79+ Data []V1OrganizationResponse `json:"Data"`
80+ }
81+
82+ // V1OrganizationCreateRequest is the request body for POST /v1/orgs
83+ type V1OrganizationCreateRequest struct {
84+ Name string `json:"Name"`
85+ Website string `json:"Website"`
86+ }
87+
7788// ClientCredentialsTokenSource implements oauth2.TokenSource for Auth0 private key JWT
7889type ClientCredentialsTokenSource struct {
7990 ctx context.Context
@@ -313,6 +324,133 @@ func getV1OrganizationFromOrgSvc(ctx context.Context, sfid string) (*V1Organizat
313324 return org , nil
314325}
315326
327+ // searchV1OrgsByNameAndWebsite searches for organizations in the v1 Organization Service by name and/or website.
328+ // Returns the first matching organization, or nil if none found.
329+ func searchV1OrgsByNameAndWebsite (ctx context.Context , name , website string ) (* V1Organization , error ) {
330+ baseURL := fmt .Sprintf ("%sorganization-service/v1/orgs/search" , cfg .LFXAPIGateway .String ())
331+ params := url.Values {}
332+ if name != "" {
333+ params .Set ("name" , name )
334+ }
335+ if website != "" {
336+ params .Add ("website-array" , website )
337+ }
338+ fullURL := baseURL + "?" + params .Encode ()
339+
340+ req , err := http .NewRequestWithContext (ctx , http .MethodGet , fullURL , nil )
341+ if err != nil {
342+ return nil , fmt .Errorf ("failed to create org search request: %w" , err )
343+ }
344+
345+ resp , err := v1HTTPClient .Do (req )
346+ if err != nil {
347+ return nil , fmt .Errorf ("failed to send org search request: %w" , err )
348+ }
349+ defer resp .Body .Close ()
350+
351+ body , err := io .ReadAll (resp .Body )
352+ if err != nil {
353+ return nil , fmt .Errorf ("failed to read org search response: %w" , err )
354+ }
355+
356+ if resp .StatusCode != http .StatusOK {
357+ return nil , fmt .Errorf ("org service returned status %d searching by name=%q website=%q: %s" , resp .StatusCode , name , website , string (body ))
358+ }
359+
360+ var listResp V1OrganizationListResponse
361+ if err := json .Unmarshal (body , & listResp ); err != nil {
362+ return nil , fmt .Errorf ("failed to unmarshal org search response: %w" , err )
363+ }
364+
365+ if len (listResp .Data ) == 0 {
366+ return nil , nil
367+ }
368+
369+ first := listResp .Data [0 ]
370+ return & V1Organization {
371+ ID : first .ID ,
372+ Name : first .Name ,
373+ Domain : first .Domain ,
374+ LastFetched : time .Now ().UTC (),
375+ }, nil
376+ }
377+
378+ // createV1OrgInOrgSvc creates a new organization in the v1 Organization Service.
379+ func createV1OrgInOrgSvc (ctx context.Context , name , website string ) (* V1Organization , error ) {
380+ apiURL := fmt .Sprintf ("%sorganization-service/v1/orgs" , cfg .LFXAPIGateway .String ())
381+
382+ reqBody , err := json .Marshal (V1OrganizationCreateRequest {Name : name , Website : website })
383+ if err != nil {
384+ return nil , fmt .Errorf ("failed to marshal org create request: %w" , err )
385+ }
386+
387+ req , err := http .NewRequestWithContext (ctx , http .MethodPost , apiURL , bytes .NewReader (reqBody ))
388+ if err != nil {
389+ return nil , fmt .Errorf ("failed to create org create request: %w" , err )
390+ }
391+ req .Header .Set ("Content-Type" , "application/json" )
392+
393+ resp , err := v1HTTPClient .Do (req )
394+ if err != nil {
395+ return nil , fmt .Errorf ("failed to send org create request: %w" , err )
396+ }
397+ defer resp .Body .Close ()
398+
399+ body , err := io .ReadAll (resp .Body )
400+ if err != nil {
401+ return nil , fmt .Errorf ("failed to read org create response: %w" , err )
402+ }
403+
404+ if resp .StatusCode != http .StatusCreated {
405+ return nil , fmt .Errorf ("org service returned status %d creating org name=%q website=%q: %s" , resp .StatusCode , name , website , string (body ))
406+ }
407+
408+ var orgResp V1OrganizationResponse
409+ if err := json .Unmarshal (body , & orgResp ); err != nil {
410+ return nil , fmt .Errorf ("failed to unmarshal org create response: %w" , err )
411+ }
412+
413+ return & V1Organization {
414+ ID : orgResp .ID ,
415+ Name : orgResp .Name ,
416+ Domain : orgResp .Domain ,
417+ LastFetched : time .Now ().UTC (),
418+ }, nil
419+ }
420+
421+ // resolveV1OrgID resolves a v1 Organization SFID from name and/or website.
422+ // It searches first; if not found and both name and website are provided, it creates a new org.
423+ func resolveV1OrgID (ctx context.Context , name , website string ) (string , error ) {
424+ if name == "" && website == "" {
425+ return "" , fmt .Errorf ("cannot resolve org: both name and website are empty" )
426+ }
427+
428+ org , err := searchV1OrgsByNameAndWebsite (ctx , name , website )
429+ if err != nil {
430+ return "" , fmt .Errorf ("org search failed: %w" , err )
431+ }
432+ if org != nil {
433+ return org .ID , nil
434+ }
435+
436+ if name == "" || website == "" {
437+ return "" , fmt .Errorf ("org not found and cannot create: missing %s" , func () string {
438+ if name == "" && website == "" {
439+ return "name and website"
440+ } else if name == "" {
441+ return "name"
442+ }
443+ return "website"
444+ }())
445+ }
446+
447+ created , err := createV1OrgInOrgSvc (ctx , name , website )
448+ if err != nil {
449+ return "" , fmt .Errorf ("org create failed: %w" , err )
450+ }
451+ return created .ID , nil
452+ }
453+
316454// getCachedV1Org retrieves an organization from the mappings KV cache
317455func getCachedV1Org (ctx context.Context , sfid string ) (* V1Organization , error ) {
318456 cacheKey := orgCacheKeyPrefix + sfid
0 commit comments