From 04dc14a93022d03d74b02ae244ee8bb8afa50c27 Mon Sep 17 00:00:00 2001 From: Amey Wale <76102448+AmeyWale@users.noreply.github.com> Date: Tue, 13 May 2025 14:36:22 +0530 Subject: [PATCH] feat(refunds_v2): Add refund update core flow in v2 apis (#7724) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 94 +++++++++++++++++++++----- crates/api_models/src/refunds.rs | 13 ++++ crates/openapi/src/openapi_v2.rs | 3 +- crates/openapi/src/routes/refunds.rs | 33 +++++++++ crates/router/src/core/refunds_v2.rs | 36 ++++++++++ crates/router/src/routes/app.rs | 6 +- crates/router/src/routes/refunds.rs | 72 ++++++++++++++++++++ crates/router/src/types/api/refunds.rs | 4 +- 8 files changed, 239 insertions(+), 22 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index c6b6f01fb0..12c30cc61f 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -3265,6 +3265,64 @@ ] } }, + "/v2/refunds/{id}/update_metadata": { + "put": { + "tags": [ + "Refunds" + ], + "summary": "Refunds - Metadata Update", + "description": "Updates the properties of a Refund object. This API can be used to attach a reason for the refund or metadata fields", + "operationId": "Update Refund Metadata and Reason", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The identifier for refund", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefundMetadataUpdateRequest" + }, + "examples": { + "Update refund reason": { + "value": { + "reason": "Paid by mistake" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Refund updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefundResponse" + } + } + } + }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, "/v2/refunds/{id}": { "get": { "tags": [ @@ -20629,6 +20687,24 @@ } } }, + "RefundMetadataUpdateRequest": { + "type": "object", + "properties": { + "reason": { + "type": "string", + "description": "An arbitrary string attached to the object. Often useful for displaying to users and your customer support executive", + "example": "Customer returned the product", + "nullable": true, + "maxLength": 255 + }, + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true + } + }, + "additionalProperties": false + }, "RefundResponse": { "type": "object", "required": [ @@ -20737,24 +20813,6 @@ "instant" ] }, - "RefundUpdateRequest": { - "type": "object", - "properties": { - "reason": { - "type": "string", - "description": "An arbitrary string attached to the object. Often useful for displaying to users and your customer support executive", - "example": "Customer returned the product", - "nullable": true, - "maxLength": 255 - }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", - "nullable": true - } - }, - "additionalProperties": false - }, "RefundsCreateRequest": { "type": "object", "required": [ diff --git a/crates/api_models/src/refunds.rs b/crates/api_models/src/refunds.rs index bb20978e0d..e556c7eccc 100644 --- a/crates/api_models/src/refunds.rs +++ b/crates/api_models/src/refunds.rs @@ -166,6 +166,19 @@ pub struct RefundUpdateRequest { pub metadata: Option, } +#[cfg(all(feature = "v2", feature = "refunds_v2"))] +#[derive(Default, Debug, ToSchema, Clone, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct RefundMetadataUpdateRequest { + /// An arbitrary string attached to the object. Often useful for displaying to users and your customer support executive + #[schema(max_length = 255, example = "Customer returned the product")] + pub reason: Option, + + /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. + #[schema(value_type = Option, example = r#"{ "city": "NY", "unit": "245" }"#)] + pub metadata: Option, +} + #[derive(Default, Debug, ToSchema, Clone, Deserialize, Serialize)] #[serde(deny_unknown_fields)] pub struct RefundManualUpdateRequest { diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index 95b6c3d7df..6057797e20 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -148,6 +148,7 @@ Never share your secret api keys. Keep them guarded and secure. //Routes for refunds routes::refunds::refunds_create, + routes::refunds::refunds_metadata_update, routes::refunds::refunds_retrieve, routes::refunds::refunds_list, @@ -193,7 +194,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::refunds::RefundType, api_models::refunds::RefundResponse, api_models::refunds::RefundStatus, - api_models::refunds::RefundUpdateRequest, + api_models::refunds::RefundMetadataUpdateRequest, api_models::organization::OrganizationCreateRequest, api_models::organization::OrganizationUpdateRequest, api_models::organization::OrganizationResponse, diff --git a/crates/openapi/src/routes/refunds.rs b/crates/openapi/src/routes/refunds.rs index 8fb6d4f183..6628d43220 100644 --- a/crates/openapi/src/routes/refunds.rs +++ b/crates/openapi/src/routes/refunds.rs @@ -112,6 +112,7 @@ pub async fn refunds_retrieve_with_body() {} operation_id = "Update a Refund", security(("api_key" = [])) )] +#[cfg(feature = "v1")] pub async fn refunds_update() {} /// Refunds - List @@ -215,6 +216,38 @@ pub async fn refunds_filter_list() {} #[cfg(feature = "v2")] pub async fn refunds_create() {} +/// Refunds - Metadata Update +/// +/// Updates the properties of a Refund object. This API can be used to attach a reason for the refund or metadata fields +#[utoipa::path( + put, + path = "/v2/refunds/{id}/update_metadata", + params( + ("id" = String, Path, description = "The identifier for refund") + ), + request_body( + content = RefundMetadataUpdateRequest, + examples( + ( + "Update refund reason" = ( + value = json!({ + "reason": "Paid by mistake" + }) + ) + ) + ) + ), + responses( + (status = 200, description = "Refund updated", body = RefundResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Refunds", + operation_id = "Update Refund Metadata and Reason", + security(("api_key" = [])) +)] +#[cfg(feature = "v2")] +pub async fn refunds_metadata_update() {} + /// Refunds - Retrieve /// /// Retrieves a Refund. This may be used to get the status of a previously initiated refund diff --git a/crates/router/src/core/refunds_v2.rs b/crates/router/src/core/refunds_v2.rs index 8d113d5a32..ee10e1b308 100644 --- a/crates/router/src/core/refunds_v2.rs +++ b/crates/router/src/core/refunds_v2.rs @@ -465,6 +465,42 @@ where request.check_integrity(request, connector_refund_id.to_owned()) } +// ********************************************** REFUND UPDATE ********************************************** + +pub async fn refund_metadata_update_core( + state: SessionState, + merchant_account: domain::MerchantAccount, + req: refunds::RefundMetadataUpdateRequest, + global_refund_id: id_type::GlobalRefundId, +) -> errors::RouterResponse { + let db = state.store.as_ref(); + let refund = db + .find_refund_by_id(&global_refund_id, merchant_account.storage_scheme) + .await + .to_not_found_response(errors::ApiErrorResponse::RefundNotFound)?; + + let response = db + .update_refund( + refund, + storage::RefundUpdate::MetadataAndReasonUpdate { + metadata: req.metadata, + reason: req.reason, + updated_by: merchant_account.storage_scheme.to_string(), + }, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable_lazy(|| { + format!( + "Unable to update refund with refund_id: {}", + global_refund_id.get_string_repr() + ) + })?; + + refunds::RefundResponse::foreign_try_from(response).map(services::ApplicationResponse::Json) +} + // ********************************************** REFUND SYNC ********************************************** #[instrument(skip_all)] diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index db65dff64b..9cb83192f6 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1187,7 +1187,11 @@ impl Refunds { { route = route .service(web::resource("").route(web::post().to(refunds::refunds_create))) - .service(web::resource("/{id}").route(web::get().to(refunds::refunds_retrieve))); + .service(web::resource("/{id}").route(web::get().to(refunds::refunds_retrieve))) + .service( + web::resource("/{id}/update_metadata") + .route(web::put().to(refunds::refunds_metadata_update)), + ); } route diff --git a/crates/router/src/routes/refunds.rs b/crates/router/src/routes/refunds.rs index dc92545841..1ae4412807 100644 --- a/crates/router/src/routes/refunds.rs +++ b/crates/router/src/routes/refunds.rs @@ -12,6 +12,39 @@ use crate::{ services::{api, authentication as auth, authorization::permissions::Permission}, types::{api::refunds, domain}, }; + +#[cfg(feature = "v2")] +/// A private module to hold internal types to be used in route handlers. +/// This is because we will need to implement certain traits on these types which will have the resource id +/// But the api payload will not contain the resource id +/// So these types can hold the resource id along with actual api payload, on which api event and locking action traits can be implemented +mod internal_payload_types { + use super::*; + + // Serialize is implemented because of api events + #[derive(Debug, serde::Serialize)] + pub struct RefundsGenericRequestWithResourceId { + pub global_refund_id: common_utils::id_type::GlobalRefundId, + pub payment_id: Option, + #[serde(flatten)] + pub payload: T, + } + + impl common_utils::events::ApiEventMetric + for RefundsGenericRequestWithResourceId + { + fn get_api_event_type(&self) -> Option { + let refund_id = self.global_refund_id.clone(); + self.payment_id + .clone() + .map(|payment_id| common_utils::events::ApiEventsType::Refund { + payment_id, + refund_id, + }) + } + } +} + /// Refunds - Create /// /// To create a refund against an already processed payment @@ -323,6 +356,45 @@ pub async fn refunds_update( .await } +#[cfg(all(feature = "v2", feature = "refunds_v2"))] +#[instrument(skip_all, fields(flow = ?Flow::RefundsUpdate))] +pub async fn refunds_metadata_update( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, + path: web::Path, +) -> HttpResponse { + let flow = Flow::RefundsUpdate; + + let global_refund_id = path.into_inner(); + let internal_payload = internal_payload_types::RefundsGenericRequestWithResourceId { + global_refund_id: global_refund_id.clone(), + payment_id: None, + payload: json_payload.into_inner(), + }; + + Box::pin(api::server_wrap( + flow, + state, + &req, + internal_payload, + |state, auth: auth::AuthenticationData, req, _| { + refund_metadata_update_core( + state, + auth.merchant_account, + req.payload, + global_refund_id.clone(), + ) + }, + &auth::V2ApiKeyAuth { + is_connected_allowed: false, + is_platform_allowed: false, + }, + api_locking::LockAction::NotApplicable, + )) + .await +} + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "refunds_v2"), diff --git a/crates/router/src/types/api/refunds.rs b/crates/router/src/types/api/refunds.rs index 935cb14c1f..908d8cddaf 100644 --- a/crates/router/src/types/api/refunds.rs +++ b/crates/router/src/types/api/refunds.rs @@ -1,11 +1,11 @@ #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "refunds_v2")))] pub use api_models::refunds::RefundRequest; -#[cfg(all(feature = "v2", feature = "refunds_v2"))] -pub use api_models::refunds::RefundsCreateRequest; pub use api_models::refunds::{ RefundListRequest, RefundListResponse, RefundResponse, RefundStatus, RefundType, RefundUpdateRequest, RefundsRetrieveBody, RefundsRetrieveRequest, }; +#[cfg(all(feature = "v2", feature = "refunds_v2"))] +pub use api_models::refunds::{RefundMetadataUpdateRequest, RefundsCreateRequest}; pub use hyperswitch_domain_models::router_flow_types::refunds::{Execute, RSync}; pub use hyperswitch_interfaces::api::refunds::{Refund, RefundExecute, RefundSync};