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>
This commit is contained in:
Amey Wale
2025-05-13 14:36:22 +05:30
committed by GitHub
parent 9c8cf93662
commit 04dc14a930
8 changed files with 239 additions and 22 deletions

View File

@ -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": [

View File

@ -166,6 +166,19 @@ pub struct RefundUpdateRequest {
pub metadata: Option<pii::SecretSerdeValue>,
}
#[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<String>,
/// 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<Object>, example = r#"{ "city": "NY", "unit": "245" }"#)]
pub metadata: Option<pii::SecretSerdeValue>,
}
#[derive(Default, Debug, ToSchema, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RefundManualUpdateRequest {

View File

@ -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,

View File

@ -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

View File

@ -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<refunds::RefundResponse> {
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)]

View File

@ -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

View File

@ -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<T: serde::Serialize> {
pub global_refund_id: common_utils::id_type::GlobalRefundId,
pub payment_id: Option<common_utils::id_type::GlobalPaymentId>,
#[serde(flatten)]
pub payload: T,
}
impl<T: serde::Serialize> common_utils::events::ApiEventMetric
for RefundsGenericRequestWithResourceId<T>
{
fn get_api_event_type(&self) -> Option<common_utils::events::ApiEventsType> {
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<AppState>,
req: HttpRequest,
json_payload: web::Json<refunds::RefundMetadataUpdateRequest>,
path: web::Path<common_utils::id_type::GlobalRefundId>,
) -> 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"),

View File

@ -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};