Repository: hyperledger/aries-cloudagent-python

Issue:
Creating an out-of-band invitation does not include goal code in invitation_url encoding
We are using `out-of-band/create-invitation` to create an OOB invitation in Traction, and the invitation URL coming back appears to lack some info that is contained in the Invitation.

POSTing the following body to `out-of-band/create-invitation`

```
{
    "accept": [
        "didcomm/aip1",
        "didcomm/aip2;env=rfc19"
    ],
    "alias": "Test Alias",
    "goal": "To issue a Faber College Graduate credential",
    "goal_code": "issue-vc",
    "handshake_protocols": [
        "https://didcomm.org/didexchange/1.0",
        "https://didcomm.org/connections/1.0"
    ],
    "my_label": "Invitation to Barry",
    "protocol_version": "1.1",
    "use_public_did": false
}
```

Returns a invitation
```
{
    "state": "initial",
    "trace": false,
    "invi_msg_id": "9840355a-3f2b-4453-949e-a5e1d0866aa7",
    "oob_id": "2fed7f3d-34e2-4e2f-8fe0-160da125fda4",
    "invitation": {
        "@type": "https://didcomm.org/out-of-band/1.1/invitation",
        "@id": "9840355a-3f2b-4453-949e-a5e1d0866aa7",
        "label": "Invitation to Barry",
        "handshake_protocols": [
            "https://didcomm.org/didexchange/1.0",
            "https://didcomm.org/connections/1.0"
        ],
        "accept": [
            "didcomm/aip1",
            "didcomm/aip2;env=rfc19"
        ],
        "services": [
            {
                "id": "#inline",
                "type": "did-communication",
                "recipientKeys": [
                    "did:key:z6MkjWpUbVaJRrfTjB6pLJRBpCRBbFmHS5xdnRUCT7JU1Wes"
                ],
                "serviceEndpoint": "https://traction-acapy-dev.apps.silver.devops.gov.bc.ca"
            }
        ],
        "goal_code": "issue-vc",
        "goal": "To issue a Faber College Graduate credential"
    },
    "invitation_url": "https://traction-acapy-dev.apps.silver.devops.gov.bc.ca?oob=eyJAdHlwZSI6ICJodHRwczovL2RpZGNvbW0ub3JnL291dC1vZi1iYW5kLzEuMS9pbnZpdGF0aW9uIiwgIkBpZCI6ICI5ODQwMzU1YS0zZjJiLTQ0NTMtOTQ5ZS1hNWUxZDA4NjZhYTciLCAibGFiZWwiOiAiSW52aXRhdGlvbiB0byBCYXJyeSIsICJoYW5kc2hha2VfcHJvdG9jb2xzIjogWyJodHRwczovL2RpZGNvbW0ub3JnL2RpZGV4Y2hhbmdlLzEuMCIsICJodHRwczovL2RpZGNvbW0ub3JnL2Nvbm5lY3Rpb25zLzEuMCJdLCAiYWNjZXB0IjogWyJkaWRjb21tL2FpcDEiLCAiZGlkY29tbS9haXAyO2Vudj1yZmMxOSJdLCAic2VydmljZXMiOiBbeyJpZCI6ICIjaW5saW5lIiwgInR5cGUiOiAiZGlkLWNvbW11bmljYXRpb24iLCAicmVjaXBpZW50S2V5cyI6IFsiZGlkOmtleTp6Nk1raldwVWJWYUpScmZUakI2cExKUkJwQ1JCYkZtSFM1eGRuUlVDVDdKVTFXZXMiXSwgInNlcnZpY2VFbmRwb2ludCI6ICJodHRwczovL3RyYWN0aW9uLWFjYXB5LWRldi5hcHBzLnNpbHZlci5kZXZvcHMuZ292LmJjLmNhIn1dfQ=="
}
```

However that invitation URL block when decoded does not include the goal information:
```
{
    "@id": "9840355a-3f2b-4453-949e-a5e1d0866aa7",
    "@type": "https://didcomm.org/out-of-band/1.1/invitation",
    "accept": [
        "didcomm/aip1",
        "didcomm/aip2;env=rfc19"
    ],
    "handshake_protocols": [
        "https://didcomm.org/didexchange/1.0",
        "https://didcomm.org/connections/1.0"
    ],
    "label": "Invitation to Barry",
    "services": [
        {
            "id": "#inline",
            "recipientKeys": [
                "did:key:z6MkjWpUbVaJRrfTjB6pLJRBpCRBbFmHS5xdnRUCT7JU1Wes"
            ],
            "serviceEndpoint": "https://traction-acapy-dev.apps.silver.devops.gov.bc.ca",
            "type": "did-communication"
        }
    ]
}
```


Discussion:
One issue I notice is that the OOB Manager is only setting the goal and goal code properties if BOTH are provided: https://github.com/hyperledger/aries-cloudagent-python/blob/37c4bc27d3c38b9843d696213d096b3fd4762dc2/aries_cloudagent/protocols/out_of_band/v1_0/manager.py#L366
Wondered if that might be it when I saw @wadeking98 ’s example.  In that, he had provided a value for the code, but the goal was blank (“”). Interesting.

However, the example above shows both fields populated, and both missing from the invitation_url value.
One line before the line that I linked, it encodes the invitation as a URL, prior to assigning the goal and goal_code properties. :)

Resolving patch:
diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/manager.py b/aries_cloudagent/protocols/out_of_band/v1_0/manager.py
--- a/aries_cloudagent/protocols/out_of_band/v1_0/manager.py
+++ b/aries_cloudagent/protocols/out_of_band/v1_0/manager.py
@@ -362,11 +362,12 @@ async def create_invitation(
                     routing_keys=routing_keys,
                 )
             ]
-            invi_url = invi_msg.to_url()
             if goal and goal_code:
                 invi_msg.goal_code = goal_code
                 invi_msg.goal = goal
 
+            invi_url = invi_msg.to_url()
+
             # Update connection record
             if conn_rec:
                 async with self.profile.session() as session:


Test patch:
diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/tests/test_manager.py b/aries_cloudagent/protocols/out_of_band/v1_0/tests/test_manager.py
--- a/aries_cloudagent/protocols/out_of_band/v1_0/tests/test_manager.py
+++ b/aries_cloudagent/protocols/out_of_band/v1_0/tests/test_manager.py
@@ -1,5 +1,6 @@
 """Test OOB Manager."""
 
+import base64
 import json
 from copy import deepcopy
 from datetime import datetime, timedelta, timezone
@@ -850,6 +851,50 @@ async def test_create_invitation_x_public_metadata(self):
                 context.exception
             )
 
+    async def test_create_invitation_goal_code(self):
+        self.profile.context.update_settings({"public_invites": True})
+        with mock.patch.object(
+            InMemoryWallet, "get_public_did", autospec=True
+        ) as mock_wallet_get_public_did:
+            mock_wallet_get_public_did.return_value = DIDInfo(
+                TestConfig.test_did,
+                TestConfig.test_verkey,
+                None,
+                method=SOV,
+                key_type=ED25519,
+            )
+            goal = "To issue a Faber College Graduate credential"
+            goal_code = "issue-vc"
+            my_label = "Invitation to Barry"
+
+            invi_rec = await self.manager.create_invitation(
+                public=False,
+                my_label=my_label,
+                protocol_version="1.1",
+                alias="Test Alias",
+                service_accept=["didcomm/aip1", "didcomm/aip2;env=rfc19"],
+                hs_protos=[test_module.HSProto.RFC23],
+                goal=goal,
+                goal_code=goal_code,
+            )
+            assert isinstance(invi_rec, InvitationRecord)
+            assert invi_rec.invitation.handshake_protocols
+            assert invi_rec.invitation.label == my_label
+            assert invi_rec.invitation.goal == goal
+            assert invi_rec.invitation.goal_code == goal_code
+            assert invi_rec.invitation_url
+
+            base64_message = invi_rec.invitation_url.split("=", maxsplit=1)[1]
+            base64_bytes = base64_message.encode("ascii")
+            message_bytes = base64.b64decode(base64_bytes)
+            data = message_bytes.decode("ascii")
+            assert data
+            invite_json = json.loads(data)
+            assert invite_json
+            assert invite_json["label"] == my_label
+            assert invite_json["goal"] == goal
+            assert invite_json["goal_code"] == goal_code
+
     async def test_wait_for_conn_rec_active_retrieve_by_id(self):
         with mock.patch.object(
             ConnRecord,