This is an automated email from the ASF dual-hosted git repository. lahirujayathilake pushed a commit to branch access-integration-v3 in repository https://gitbox.apache.org/repos/asf/airavata-custos.git
commit dbf2d3ff1de3f078f177e6e5dd6a81ca5ed8ffc5 Author: lahiruj <[email protected]> AuthorDate: Thu May 21 02:12:35 2026 -0400 Use Keep/DeleteGlobalID for request_person_merge and baseline amie test packets --- .../AMIE-Processor/handler/request_person_merge.go | 40 ++++---- .../AMIE-Processor/mock-server/mock-amie-server.py | 114 ++++++++++++++++++++- .../request_person_merge/incoming-request.json | 6 +- 3 files changed, 134 insertions(+), 26 deletions(-) diff --git a/connectors/ACCESS/AMIE-Processor/handler/request_person_merge.go b/connectors/ACCESS/AMIE-Processor/handler/request_person_merge.go index 8ddd43d2c..1248bbb96 100644 --- a/connectors/ACCESS/AMIE-Processor/handler/request_person_merge.go +++ b/connectors/ACCESS/AMIE-Processor/handler/request_person_merge.go @@ -24,7 +24,6 @@ import ( "log/slog" "github.com/apache/airavata-custos/connectors/ACCESS/AMIE-Processor/model" - "github.com/apache/airavata-custos/pkg/models" "github.com/apache/airavata-custos/pkg/service" ) @@ -45,44 +44,49 @@ func (h *RequestPersonMergeHandler) Handle(ctx context.Context, tx *sql.Tx, pack if err != nil { return err } - primaryGlobalID := getString(body, "PrimaryGlobalID") - if err := requireText(primaryGlobalID, "PrimaryGlobalID"); err != nil { + keepGlobalID := getString(body, "KeepGlobalID") + if err := requireText(keepGlobalID, "KeepGlobalID"); err != nil { return err } - secondaryGlobalID := getString(body, "SecondaryGlobalID") - if err := requireText(secondaryGlobalID, "SecondaryGlobalID"); err != nil { + deleteGlobalID := getString(body, "DeleteGlobalID") + if err := requireText(deleteGlobalID, "DeleteGlobalID"); err != nil { return err } - if primaryGlobalID == secondaryGlobalID { - return fmt.Errorf("request_person_merge: primary and secondary global IDs are identical") + if keepGlobalID == deleteGlobalID { + return fmt.Errorf("request_person_merge: keep and delete global IDs are identical") } - survivor, err := h.svc.GetUserByExternalIdentity(ctx, amieIdentitySource, primaryGlobalID) + survivor, err := h.svc.GetUserByExternalIdentity(ctx, amieIdentitySource, keepGlobalID) if err != nil { return fmt.Errorf("request_person_merge: resolve surviving user: %w", err) } - retiring, err := h.svc.GetUserByExternalIdentity(ctx, amieIdentitySource, secondaryGlobalID) + retiring, err := h.svc.GetUserByExternalIdentity(ctx, amieIdentitySource, deleteGlobalID) if err != nil { return fmt.Errorf("request_person_merge: resolve retiring user: %w", err) } - // Refuse to merge while the retiring user still has active memberships: - // AMIE is expected to inactivate the user's accounts first, and merging - // over active state would silently transfer ownership of live resources. memberships, err := h.svc.ListAllocationsForUser(ctx, retiring.ID) if err != nil { return fmt.Errorf("request_person_merge: list memberships: %w", err) } + activeMemberships := make([]string, 0, len(memberships)) for _, m := range memberships { - if m.MembershipStatus == models.ACTIVE { - return fmt.Errorf("request_person_merge: retiring user %s has active membership %s; inactivate first", - retiring.ID, m.ID) + if m.MembershipStatus == "ACTIVE" { + activeMemberships = append(activeMemberships, m.ID) } } + if len(activeMemberships) > 0 { + slog.WarnContext(ctx, "merging user with active memberships", + "keep_global_id", keepGlobalID, + "delete_global_id", deleteGlobalID, + "retiring_id", retiring.ID, + "active_membership_ids", activeMemberships, + ) + } - slog.WarnContext(ctx, "executing user merge", - "primary_global_id", primaryGlobalID, - "secondary_global_id", secondaryGlobalID, + slog.InfoContext(ctx, "executing user merge", + "keep_global_id", keepGlobalID, + "delete_global_id", deleteGlobalID, "survivor_id", survivor.ID, "retiring_id", retiring.ID, ) diff --git a/connectors/ACCESS/AMIE-Processor/mock-server/mock-amie-server.py b/connectors/ACCESS/AMIE-Processor/mock-server/mock-amie-server.py index 8b331ddc9..c3e495c08 100644 --- a/connectors/ACCESS/AMIE-Processor/mock-server/mock-amie-server.py +++ b/connectors/ACCESS/AMIE-Processor/mock-server/mock-amie-server.py @@ -195,10 +195,10 @@ def gen_valid_person_merge(): primary = str(random.randint(100000, 999999)) secondary = str(random.randint(100000, 999999)) return make_packet("request_person_merge", { + "KeepGlobalID": primary, "KeepPersonID": f"person-keep-{uuid.uuid4().hex[:8]}", + "DeleteGlobalID": secondary, "DeletePersonID": f"person-delete-{uuid.uuid4().hex[:8]}", - "PrimaryGlobalID": primary, - "SecondaryGlobalID": secondary, "MergeReason": "Duplicate person records", }) @@ -426,16 +426,120 @@ def generate_all_handlers_once(): gen_valid_account_inactivate(), gen_valid_account_reactivate(), make_packet("request_person_merge", { + "KeepGlobalID": primary_gid, "KeepPersonID": f"person-keep-{uuid.uuid4().hex[:8]}", + "DeleteGlobalID": secondary_gid, "DeletePersonID": f"person-delete-{uuid.uuid4().hex[:8]}", - "PrimaryGlobalID": primary_gid, - "SecondaryGlobalID": secondary_gid, "MergeReason": "all_handlers test scenario", }), gen_inform_transaction_complete(), ] +# Deterministic baseline scenario. Skips handlers that need a Custos UUID +# (request_account_create, the inactivate/reactivate handlers). + +def gen_baseline_scenario(): + return [ + make_packet("request_project_create", { + "GrantNumber": "BL-001", + "PfosNumber": "PFOS-BL-001", + "ProjectTitle": "Baseline Project 1", + "PiGlobalID": "bl-pi-001", + "PiFirstName": "Pat", + "PiLastName": "First", + "PiEmail": "[email protected]", + "PiOrganization": "Baseline Org", + "PiOrgCode": "BASELINE", + "NsfStatusCode": "AC", + "PiDnList": ["/C=US/O=Baseline Org/CN=Pat First"], + "ServiceUnitsAllocated": "10000", + "StartDate": "2026-01-01", + "EndDate": "2026-12-31", + "ResourceList": ["baseline-cluster.example.edu"], + "AllocationType": "new", + }), + make_packet("request_project_create", { + "GrantNumber": "BL-002", + "PfosNumber": "PFOS-BL-002", + "ProjectTitle": "Baseline Project 2", + "PiGlobalID": "bl-pi-002", + "PiFirstName": "Sam", + "PiLastName": "Second", + "PiEmail": "[email protected]", + "PiOrganization": "Baseline Org", + "PiOrgCode": "BASELINE", + "NsfStatusCode": "AC", + "PiDnList": [], + "ServiceUnitsAllocated": "20000", + "StartDate": "2026-01-01", + "EndDate": "2026-12-31", + "ResourceList": ["baseline-cluster.example.edu"], + "AllocationType": "new", + }), + # Re-delivery of BL-001 as a supplement; writes a compute_allocation_diffs row. + make_packet("request_project_create", { + "GrantNumber": "BL-001", + "PfosNumber": "PFOS-BL-001", + "ProjectTitle": "Baseline Project 1", + "PiGlobalID": "bl-pi-001", + "PiFirstName": "Pat", + "PiLastName": "First", + "PiEmail": "[email protected]", + "PiOrganization": "Baseline Org", + "PiOrgCode": "BASELINE", + "NsfStatusCode": "AC", + "ServiceUnitsAllocated": "5000", + "StartDate": "2026-01-01", + "EndDate": "2026-12-31", + "ResourceList": ["baseline-cluster.example.edu"], + "AllocationType": "supplement", + }), + make_packet("data_project_create", { + "ProjectID": "BL-001", + "PersonID": "bl-pi-001-person", + "GlobalID": "bl-pi-001", + "DnList": [ + "/C=US/O=Baseline Org/CN=Pat First Extra", + "/DC=EDU/CN=patfirst", + ], + }), + make_packet("data_account_create", { + "ProjectID": "BL-002", + "PersonID": "bl-pi-002-person", + "GlobalID": "bl-pi-002", + "DnList": [ + "/C=US/O=Baseline Org/CN=Sam Second", + ], + }), + make_packet("request_user_modify", { + "ActionType": "replace", + "ProjectID": "BL-001", + "PersonID": "bl-pi-001-person", + "UserPersonID": "bl-pi-001-user", + "UserGlobalID": "bl-pi-001", + "UserFirstName": "Pat", + "UserLastName": "First-Updated", + "UserEmail": "[email protected]", + "UserOrganization": "Baseline Org", + "UserOrgCode": "BASELINE", + "NsfStatusCode": "AC", + }), + make_packet("request_person_merge", { + "KeepGlobalID": "bl-pi-001", + "KeepPersonID": "bl-keep-person", + "DeleteGlobalID": "bl-pi-002", + "DeletePersonID": "bl-delete-person", + "MergeReason": "Duplicate person records", + }), + make_packet("inform_transaction_complete", { + "StatusCode": "Success", + "Message": "Baseline complete", + "DetailCode": "1", + }), + ] + + # ----- API endpoints ----- @app.route("/packets/<site>", methods=["GET"]) @@ -482,6 +586,8 @@ def create_scenario(site): packets = generate_all_handlers_once() elif scenario_type == "dev_email": packets = gen_dev_email_scenario() + elif scenario_type == "baseline": + packets = gen_baseline_scenario() else: packets = generate_batch(success_count=3, failure_count=2) pending_packets.extend(packets) diff --git a/connectors/ACCESS/AMIE-Processor/testdata/request_person_merge/incoming-request.json b/connectors/ACCESS/AMIE-Processor/testdata/request_person_merge/incoming-request.json index 5ffdfb697..0ef136352 100644 --- a/connectors/ACCESS/AMIE-Processor/testdata/request_person_merge/incoming-request.json +++ b/connectors/ACCESS/AMIE-Processor/testdata/request_person_merge/incoming-request.json @@ -23,12 +23,10 @@ "transaction_state": "in-progress" }, "body": { + "KeepGlobalID": "12345", "KeepPersonID": "test-person-primary-123", + "DeleteGlobalID": "67890", "DeletePersonID": "test-person-secondary-456", - "PrimaryGlobalID": "12345", - "SecondaryGlobalID": "67890", - "PrimarySitePersonID": "primary-user", - "SecondarySitePersonID": "secondary-user", "MergeReason": "Duplicate person records", "Comment": "Person merge for testing" }
