diff --git a/src/openforms/registrations/base.py b/src/openforms/registrations/base.py index 3ba4e0cfa9..f18c0562a6 100644 --- a/src/openforms/registrations/base.py +++ b/src/openforms/registrations/base.py @@ -49,7 +49,9 @@ class BasePlugin(ABC, AbstractBasePlugin): def register_submission(self, submission: Submission, options: dict) -> dict | None: raise NotImplementedError() - def update_payment_status(self, submission: Submission, options: dict): + def update_payment_status( + self, submission: Submission, options: dict + ) -> dict | None: raise NotImplementedError() def pre_register_submission( diff --git a/src/openforms/registrations/contrib/objects_api/plugin.py b/src/openforms/registrations/contrib/objects_api/plugin.py index c4094bffa3..2a3ba23e81 100644 --- a/src/openforms/registrations/contrib/objects_api/plugin.py +++ b/src/openforms/registrations/contrib/objects_api/plugin.py @@ -10,7 +10,6 @@ from typing_extensions import override from openforms.registrations.utils import execute_unless_result_exists -from openforms.utils.date import get_today from openforms.variables.service import get_static_variables from ...base import BasePlugin @@ -108,7 +107,7 @@ def get_custom_templatetags_libraries(self) -> list[str]: @override def update_payment_status( self, submission: Submission, options: RegistrationOptions - ) -> None: + ) -> dict[str, Any] | None: config = ObjectsAPIConfig.get_solo() assert isinstance(config, ObjectsAPIConfig) config.apply_defaults_to(options) @@ -121,21 +120,19 @@ def update_payment_status( if updated_object_data is None: return - updated_object_data = { - "record": { - "data": updated_object_data, - "startAt": get_today(), - }, - } - object_url = submission.registration_result["url"] with get_objects_client() as objects_client: - response = objects_client.patch( + operation = ( + objects_client.patch if options["version"] == 1 else objects_client.put + ) + + response = operation( url=object_url, json=updated_object_data, headers={"Content-Crs": "EPSG:4326"}, ) response.raise_for_status() + return response.json() @override def get_variables(self) -> list[FormVariable]: diff --git a/src/openforms/registrations/contrib/objects_api/submission_registration.py b/src/openforms/registrations/contrib/objects_api/submission_registration.py index 9b7d2e01db..b07aaf0978 100644 --- a/src/openforms/registrations/contrib/objects_api/submission_registration.py +++ b/src/openforms/registrations/contrib/objects_api/submission_registration.py @@ -241,6 +241,7 @@ def get_object_data( def get_update_payment_status_data( self, submission: Submission, options: OptionsT ) -> dict[str, Any] | None: + """Get the object data payload to be sent (either as a PATCH or PUT request) to the Objects API.""" pass @@ -320,7 +321,16 @@ def get_update_payment_status_data( "payment": self.get_payment_context_data(submission), } - return render_to_json(options["payment_status_update_json"], context) + record_data = cast( + dict[str, Any], + render_to_json(options["payment_status_update_json"], context), + ) + + return prepare_data_for_registration( + record_data=record_data, + objecttype=options["objecttype"], + objecttype_version=options["objecttype_version"], + ) class ObjectsAPIV2Handler(ObjectsAPIRegistrationHandler[RegistrationOptionsV2]): @@ -381,9 +391,11 @@ def get_object_data( @override def get_update_payment_status_data( self, submission: Submission, options: RegistrationOptionsV2 - ) -> None: - # TODO - return None + ) -> dict[str, Any]: + # In V2, a PUT request is made, so we essentially return the same + # payload from the initial registration. Payment related variables + # will have their value updated. + return self.get_object_data(submission, options) HANDLER_MAPPING: dict[ConfigVersion, ObjectsAPIRegistrationHandler[Any]] = { diff --git a/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPaymentStatusUpdateV2Tests/ObjectsAPIPaymentStatusUpdateV2Tests.test_update_payment_status.yaml b/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPaymentStatusUpdateV2Tests/ObjectsAPIPaymentStatusUpdateV2Tests.test_update_payment_status.yaml new file mode 100644 index 0000000000..865d837e8c --- /dev/null +++ b/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPaymentStatusUpdateV2Tests/ObjectsAPIPaymentStatusUpdateV2Tests.test_update_payment_status.yaml @@ -0,0 +1,103 @@ +interactions: +- request: + body: '{"type": "http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", + "record": {"typeVersion": 3, "data": {"age": 20, "name": {"last.name": "My last + name"}, "submission_pdf_url": "http://example.com", "submission_csv_url": "http://example.com", + "submission_payment_completed": false, "submission_payment_amount": "0", "submission_payment_public_ids": + [], "submission_date": "2020-02-02T00:00:00+00:00"}, "startAt": "2020-02-02"}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '456' + Content-Type: + - application/json + User-Agent: + - python-requests/2.31.0 + method: POST + uri: http://localhost:8002/api/v2/objects + response: + body: + string: '{"url":"http://localhost:8002/api/v2/objects/2c287cd4-7737-473d-84de-8b0a018a46bb","uuid":"2c287cd4-7737-473d-84de-8b0a018a46bb","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"age":20,"name":{"last.name":"My + last name"},"submission_pdf_url":"http://example.com","submission_csv_url":"http://example.com","submission_payment_completed":false,"submission_payment_amount":"0","submission_payment_public_ids":[],"submission_date":"2020-02-02T00:00:00+00:00"},"geometry":null,"startAt":"2020-02-02","endAt":null,"registrationAt":"2024-03-19","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, POST, HEAD, OPTIONS + Content-Crs: + - EPSG:4326 + Content-Length: + - '669' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Location: + - http://localhost:8002/api/v2/objects/2c287cd4-7737-473d-84de-8b0a018a46bb + Referrer-Policy: + - same-origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 201 + message: Created +- request: + body: '{"type": "http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", + "record": {"typeVersion": 3, "data": {"age": 20, "name": {"last.name": "My last + name"}, "submission_date": "2020-02-02T00:00:00+00:00", "submission_pdf_url": + "", "submission_csv_url": "", "submission_payment_completed": true, "submission_payment_amount": + "10.01", "submission_payment_public_ids": ["TEST-123"]}, "startAt": "2020-02-02", + "geometry": {"type": "Point", "coordinates": [52.36673378967122, 4.893164274470299]}}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '519' + Content-Type: + - application/json + User-Agent: + - python-requests/2.31.0 + method: PUT + uri: http://localhost:8002/api/v2/objects/2c287cd4-7737-473d-84de-8b0a018a46bb + response: + body: + string: '{"url":"http://localhost:8002/api/v2/objects/2c287cd4-7737-473d-84de-8b0a018a46bb","uuid":"2c287cd4-7737-473d-84de-8b0a018a46bb","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":2,"typeVersion":3,"data":{"age":20,"name":{"last.name":"My + last name"},"submission_date":"2020-02-02T00:00:00+00:00","submission_pdf_url":"","submission_csv_url":"","submission_payment_completed":true,"submission_payment_amount":"10.01","submission_payment_public_ids":["TEST-123"]},"geometry":{"type":"Point","coordinates":[52.36673378967122,4.893164274470299]},"startAt":"2020-02-02","endAt":null,"registrationAt":"2024-03-19","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Content-Crs: + - EPSG:4326 + Content-Length: + - '710' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Referrer-Policy: + - same-origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +version: 1 diff --git a/src/openforms/registrations/contrib/objects_api/tests/test_update_payment_status.py b/src/openforms/registrations/contrib/objects_api/tests/test_update_payment_status_v1.py similarity index 95% rename from src/openforms/registrations/contrib/objects_api/tests/test_update_payment_status.py rename to src/openforms/registrations/contrib/objects_api/tests/test_update_payment_status_v1.py index e2205c9efc..3dfcae1b4d 100644 --- a/src/openforms/registrations/contrib/objects_api/tests/test_update_payment_status.py +++ b/src/openforms/registrations/contrib/objects_api/tests/test_update_payment_status_v1.py @@ -17,7 +17,7 @@ @requests_mock.Mocker() -class ObjectsAPIPaymentStatusUpdateTests(TestCase): +class ObjectsAPIPaymentStatusUpdateV1Tests(TestCase): def test_update_payment_status(self, m): submission = SubmissionFactory.from_components( [ @@ -59,6 +59,7 @@ def test_update_payment_status(self, m): m.patch( "https://objecten.nl/api/v1/objects/111-222-333", + json={}, # Unused in our case, but required as .json() is called on the response status_code=200, ) @@ -117,11 +118,11 @@ def test_template_overwritten_through_options(self, m): m.patch( "https://objecten.nl/api/v1/objects/111-222-333", + json={}, # Unused in our case, but required as .json() is called on the response status_code=200, ) plugin = ObjectsAPIRegistration(PLUGIN_IDENTIFIER) - with freeze_time("2020-02-02"): with patch( "openforms.registrations.contrib.objects_api.models.ObjectsAPIConfig.get_solo", @@ -190,6 +191,7 @@ def test_no_template_specified(self, m): m.patch( "https://objecten.nl/api/v1/objects/111-222-333", + json={}, # Unused in our case, but required as .json() is called on the response status_code=200, ) diff --git a/src/openforms/registrations/contrib/objects_api/tests/test_update_payment_status_v2.py b/src/openforms/registrations/contrib/objects_api/tests/test_update_payment_status_v2.py new file mode 100644 index 0000000000..fded17a157 --- /dev/null +++ b/src/openforms/registrations/contrib/objects_api/tests/test_update_payment_status_v2.py @@ -0,0 +1,168 @@ +from pathlib import Path +from unittest.mock import patch + +from django.test import TestCase +from django.utils import timezone + +from freezegun import freeze_time +from zgw_consumers.constants import APITypes, AuthTypes +from zgw_consumers.test.factories import ServiceFactory + +from openforms.contrib.objects_api.helpers import prepare_data_for_registration +from openforms.payments.constants import PaymentStatus +from openforms.payments.tests.factories import SubmissionPaymentFactory +from openforms.submissions.tests.factories import SubmissionFactory +from openforms.utils.tests.vcr import OFVCRMixin + +from ..client import get_objects_client +from ..models import ObjectsAPIConfig, ObjectsAPIRegistrationData +from ..plugin import PLUGIN_IDENTIFIER, ObjectsAPIRegistration +from ..typing import RegistrationOptionsV2 + + +@freeze_time("2020-02-02") +class ObjectsAPIPaymentStatusUpdateV2Tests(OFVCRMixin, TestCase): + + VCR_TEST_FILES = Path(__file__).parent / "files" + + def setUp(self): + super().setUp() + + config = ObjectsAPIConfig( + objects_service=ServiceFactory.build( + api_root="http://localhost:8002/api/v2/", + api_type=APITypes.orc, + oas="https://example.com/", + header_key="Authorization", + # See the docker compose fixtures: + header_value="Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9", + auth_type=AuthTypes.api_key, + ), + ) + + config_patcher = patch( + "openforms.registrations.contrib.objects_api.models.ObjectsAPIConfig.get_solo", + return_value=config, + ) + self.mock_get_config = config_patcher.start() + self.addCleanup(config_patcher.stop) + + def test_update_payment_status(self): + # We manually create the objects instance, to be in the same state after + # `plugin.register_submission` was called: + with get_objects_client() as client: + data = client.create_object( + object_data=prepare_data_for_registration( + record_data={ + "age": 20, + "name": { + "last.name": "My last name", + }, + "submission_pdf_url": "http://example.com", + "submission_csv_url": "http://example.com", + "submission_payment_completed": False, + "submission_payment_amount": "0", + "submission_payment_public_ids": [], + "submission_date": timezone.now().isoformat(), + }, + objecttype="http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", + objecttype_version=3, + ) + ) + objects_url = data["url"] + + submission = SubmissionFactory.from_components( + [ + # fmt: off + { + "key": "age", + "type": "number" + }, + { + "key": "lastname", + "type": "textfield", + }, + { + "key": "location", + "type": "map", + }, + # fmt: on + ], + registration_success=True, + submitted_data={ + "age": 20, + "lastname": "My last name", + "location": [52.36673378967122, 4.893164274470299], + }, + registration_result={"url": objects_url}, + ) + + ObjectsAPIRegistrationData.objects.create(submission=submission) + + v2_options: RegistrationOptionsV2 = { + "version": 2, + # See the docker compose fixtures for more info on these values: + "objecttype": "http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", + "objecttype_version": 3, + "upload_submission_csv": True, + "informatieobjecttype_submission_report": "http://localhost:8003/catalogi/api/v1/informatieobjecttypen/7a474713-0833-402a-8441-e467c08ac55b", + "informatieobjecttype_submission_csv": "http://localhost:8003/catalogi/api/v1/informatieobjecttypen/b2d83b94-9b9b-4e80-a82f-73ff993c62f3", + "informatieobjecttype_attachment": "http://localhost:8003/catalogi/api/v1/informatieobjecttypen/531f6c1a-97f7-478c-85f0-67d2f23661c7", + "organisatie_rsin": "000000000", + "variables_mapping": [ + # fmt: off + { + "variable_key": "age", + "target_path": ["age"], + }, + { + "variable_key": "lastname", + "target_path": ["name", "last.name"] + }, + { + "variable_key": "now", + "target_path": ["submission_date"], + }, + { + "variable_key": "pdf_url", + "target_path": ["submission_pdf_url"], + }, + { + "variable_key": "csv_url", + "target_path": ["submission_csv_url"], + }, + { + "variable_key": "payment_completed", + "target_path": ["submission_payment_completed"], + }, + { + "variable_key": "payment_amount", + "target_path": ["submission_payment_amount"], + }, + { + "variable_key": "payment_public_order_ids", + "target_path": ["submission_payment_public_ids"], + }, + # fmt: on + ], + "geometry_variable_key": "location", + } + + SubmissionPaymentFactory.create( + submission=submission, + status=PaymentStatus.completed, + amount=10.01, + public_order_id="TEST-123", + ) + + plugin = ObjectsAPIRegistration(PLUGIN_IDENTIFIER) + + result = plugin.update_payment_status(submission, v2_options) + + assert result is not None + + result_data = result["record"]["data"] + + self.assertTrue(result_data["submission_payment_completed"]) + self.assertEqual(result_data["submission_payment_amount"], "10.01") + self.assertEqual(result_data["submission_payment_public_ids"], ["TEST-123"])