From c333fb7fc02cf19d74ca80093552e4c4628f248a Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Wed, 9 Aug 2023 01:56:27 +0530 Subject: [PATCH] feat(router): add support for multiple partial capture (#1721) Co-authored-by: hrithikeshvm Co-authored-by: Arun Raj M --- Cargo.lock | 282 +++--------------- crates/common_enums/src/enums.rs | 29 ++ crates/diesel_models/src/capture.rs | 121 ++++++++ crates/diesel_models/src/enums.rs | 8 +- crates/diesel_models/src/lib.rs | 1 + crates/diesel_models/src/payment_attempt.rs | 15 + crates/diesel_models/src/query.rs | 1 + crates/diesel_models/src/query/capture.rs | 78 +++++ crates/diesel_models/src/schema.rs | 35 +++ crates/router/build.rs | 2 +- .../stripe/payment_intents/types.rs | 7 +- .../stripe/setup_intents/types.rs | 3 +- crates/router/src/core/payments.rs | 94 +++++- crates/router/src/core/payments/helpers.rs | 10 +- .../payments/operations/payment_cancel.rs | 1 + .../payments/operations/payment_capture.rs | 114 ++++++- .../operations/payment_complete_authorize.rs | 1 + .../payments/operations/payment_confirm.rs | 1 + .../payments/operations/payment_create.rs | 1 + .../operations/payment_method_validate.rs | 1 + .../payments/operations/payment_response.rs | 198 ++++++++---- .../payments/operations/payment_session.rs | 1 + .../core/payments/operations/payment_start.rs | 1 + .../payments/operations/payment_status.rs | 1 + .../payments/operations/payment_update.rs | 1 + .../router/src/core/payments/transformers.rs | 4 + crates/router/src/db.rs | 4 + crates/router/src/db/capture.rs | 220 ++++++++++++++ crates/router/src/db/payment_attempt.rs | 2 + crates/router/src/types.rs | 1 + crates/router/src/types/storage.rs | 5 +- crates/router/src/types/storage/capture.rs | 1 + .../src/types/storage/connector_response.rs | 31 ++ .../src/types/storage/payment_attempt.rs | 44 +++ crates/router/src/types/transformers.rs | 43 ++- .../down.sql | 15 + .../up.sql | 38 +++ openapi/openapi_spec.json | 3 +- 38 files changed, 1082 insertions(+), 336 deletions(-) create mode 100644 crates/diesel_models/src/capture.rs create mode 100644 crates/diesel_models/src/query/capture.rs create mode 100644 crates/router/src/db/capture.rs create mode 100644 crates/router/src/types/storage/capture.rs create mode 100644 migrations/2023-07-07-091223_create_captures_table/down.sql create mode 100644 migrations/2023-07-07-091223_create_captures_table/up.sql diff --git a/Cargo.lock b/Cargo.lock index 1840cefa6b..c020039a10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -297,12 +297,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "adler32" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" - [[package]] name = "ahash" version = "0.7.6" @@ -385,14 +379,15 @@ dependencies = [ "cards", "common_enums", "common_utils", - "error-stack", + "frunk", + "frunk_core", "masking", "mime", "reqwest", "router_derive", "serde", "serde_json", - "strum 0.24.1", + "strum", "time 0.3.22", "url", "utoipa", @@ -1165,12 +1160,6 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" -[[package]] -name = "bytemuck" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" - [[package]] name = "byteorder" version = "1.4.3" @@ -1287,12 +1276,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "checked_int_cast" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919" - [[package]] name = "chrono" version = "0.4.26" @@ -1349,23 +1332,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - [[package]] name = "common_enums" version = "0.1.0" dependencies = [ - "common_utils", "diesel", "router_derive", "serde", "serde_json", - "strum 0.25.0", - "time 0.3.22", + "strum", "utoipa", ] @@ -1518,17 +1493,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "crossbeam-deque" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" -dependencies = [ - "cfg-if", - "crossbeam-epoch", - "crossbeam-utils", -] - [[package]] name = "crossbeam-epoch" version = "0.9.15" @@ -1663,16 +1627,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaa37046cc0f6c3cc6090fbdbf73ef0b8ef4cfcc37f6befc0020f63e8cf121e1" -[[package]] -name = "deflate" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" -dependencies = [ - "adler32", - "byteorder", -] - [[package]] name = "derive_deref" version = "1.1.1" @@ -1725,27 +1679,6 @@ dependencies = [ "syn 2.0.18", ] -[[package]] -name = "diesel_models" -version = "0.1.0" -dependencies = [ - "async-bb8-diesel", - "common_enums", - "common_utils", - "diesel", - "error-stack", - "frunk", - "frunk_core", - "masking", - "router_derive", - "router_env", - "serde", - "serde_json", - "strum 0.24.1", - "thiserror", - "time 0.3.22", -] - [[package]] name = "diesel_table_macro_syntax" version = "0.1.0" @@ -1808,7 +1741,6 @@ dependencies = [ "common_utils", "config", "diesel", - "diesel_models", "error-stack", "external_services", "masking", @@ -1818,6 +1750,7 @@ dependencies = [ "serde", "serde_json", "serde_path_to_error", + "storage_models", "thiserror", "tokio", ] @@ -1978,7 +1911,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" dependencies = [ "crc32fast", - "miniz_oxide 0.7.1", + "miniz_oxide", ] [[package]] @@ -2266,16 +2199,6 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] -[[package]] -name = "gif" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" -dependencies = [ - "color_quant", - "weezl", -] - [[package]] name = "git2" version = "0.17.2" @@ -2541,25 +2464,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "image" -version = "0.23.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" -dependencies = [ - "bytemuck", - "byteorder", - "color_quant", - "gif", - "jpeg-decoder", - "num-iter", - "num-rational", - "num-traits", - "png", - "scoped_threadpool", - "tiff", -] - [[package]] name = "indexmap" version = "1.9.3" @@ -2664,15 +2568,6 @@ dependencies = [ "time 0.3.22", ] -[[package]] -name = "jpeg-decoder" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" -dependencies = [ - "rayon", -] - [[package]] name = "js-sys" version = "0.3.64" @@ -2956,25 +2851,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" -dependencies = [ - "adler32", -] - -[[package]] -name = "miniz_oxide" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" -dependencies = [ - "adler", - "autocfg", -] - [[package]] name = "miniz_oxide" version = "0.7.1" @@ -3089,28 +2965,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-iter" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.15" @@ -3452,18 +3306,6 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" -[[package]] -name = "png" -version = "0.16.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "deflate", - "miniz_oxide 0.3.7", -] - [[package]] name = "polling" version = "2.8.0" @@ -3598,16 +3440,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "qrcode" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d2f1455f3630c6e5107b4f2b94e74d76dea80736de0981fd27644216cff57f" -dependencies = [ - "checked_int_cast", - "image", -] - [[package]] name = "quanta" version = "0.11.1" @@ -3749,28 +3581,6 @@ dependencies = [ "bitflags 1.3.2", ] -[[package]] -name = "rayon" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-utils", - "num_cpus", -] - [[package]] name = "redis-protocol" version = "4.1.0" @@ -3971,15 +3781,15 @@ dependencies = [ "crc32fast", "derive_deref", "diesel", - "diesel_models", "dyn-clone", "encoding_rs", "error-stack", "external_services", + "frunk", + "frunk_core", "futures", "hex", "http", - "image", "infer 0.13.0", "josekit", "jsonwebtoken", @@ -3992,7 +3802,6 @@ dependencies = [ "nanoid", "num_cpus", "once_cell", - "qrcode", "rand 0.8.5", "redis_interface", "regex", @@ -4010,7 +3819,8 @@ dependencies = [ "serial_test", "signal-hook", "signal-hook-tokio", - "strum 0.24.1", + "storage_models", + "strum", "test_utils", "thirtyfour", "thiserror", @@ -4035,7 +3845,7 @@ dependencies = [ "quote", "serde", "serde_json", - "strum 0.24.1", + "strum", "syn 1.0.109", ] @@ -4053,7 +3863,7 @@ dependencies = [ "serde", "serde_json", "serde_path_to_error", - "strum 0.24.1", + "strum", "time 0.3.22", "tokio", "tracing", @@ -4232,12 +4042,6 @@ dependencies = [ "parking_lot", ] -[[package]] -name = "scoped_threadpool" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" - [[package]] name = "scopeguard" version = "1.1.0" @@ -4583,6 +4387,27 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "storage_models" +version = "0.1.0" +dependencies = [ + "async-bb8-diesel", + "common_enums", + "common_utils", + "diesel", + "error-stack", + "frunk", + "frunk_core", + "masking", + "router_derive", + "router_env", + "serde", + "serde_json", + "strum", + "thiserror", + "time 0.3.22", +] + [[package]] name = "stringmatch" version = "0.4.0" @@ -4604,16 +4429,7 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" dependencies = [ - "strum_macros 0.24.3", -] - -[[package]] -name = "strum" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" -dependencies = [ - "strum_macros 0.25.1", + "strum_macros", ] [[package]] @@ -4629,19 +4445,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "strum_macros" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6069ca09d878a33f883cc06aaa9718ede171841d3832450354410b718b097232" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.18", -] - [[package]] name = "subtle" version = "2.4.1" @@ -4835,17 +4638,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "tiff" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" -dependencies = [ - "jpeg-decoder", - "miniz_oxide 0.4.4", - "weezl", -] - [[package]] name = "time" version = "0.1.45" @@ -5572,12 +5364,6 @@ dependencies = [ "webpki", ] -[[package]] -name = "weezl" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" - [[package]] name = "winapi" version = "0.3.9" diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 0e658f4692..53e9f66928 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -85,6 +85,34 @@ pub enum AuthenticationType { NoThreeDs, } +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, + Hash, +)] +#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum CaptureStatus { + // Capture request initiated + #[default] + Started, + // Capture request was successful + Charged, + // Capture is pending at connector side + Pending, + // Capture request failed + Failed, +} + #[derive( Clone, Copy, @@ -756,6 +784,7 @@ pub enum IntentStatus { #[default] RequiresConfirmation, RequiresCapture, + PartiallyCaptured, } #[derive( diff --git a/crates/diesel_models/src/capture.rs b/crates/diesel_models/src/capture.rs new file mode 100644 index 0000000000..a747960b13 --- /dev/null +++ b/crates/diesel_models/src/capture.rs @@ -0,0 +1,121 @@ +use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; +use serde::{Deserialize, Serialize}; +use time::PrimitiveDateTime; + +use crate::{enums as storage_enums, schema::captures}; + +#[derive(Clone, Debug, Eq, PartialEq, Identifiable, Queryable, Serialize, Deserialize)] +#[diesel(table_name = captures)] +#[diesel(primary_key(capture_id))] +pub struct Capture { + pub capture_id: String, + pub payment_id: String, + pub merchant_id: String, + pub status: storage_enums::CaptureStatus, + pub amount: i64, + pub currency: Option, + pub connector: Option, + pub error_message: Option, + pub error_code: Option, + pub error_reason: Option, + pub tax_amount: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub modified_at: PrimitiveDateTime, + pub authorized_attempt_id: String, + pub connector_transaction_id: Option, + pub capture_sequence: i16, +} + +#[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay, Serialize, Deserialize)] +#[diesel(table_name = captures)] +pub struct CaptureNew { + pub capture_id: String, + pub payment_id: String, + pub merchant_id: String, + pub status: storage_enums::CaptureStatus, + pub amount: i64, + pub currency: Option, + pub connector: Option, + pub error_message: Option, + pub error_code: Option, + pub error_reason: Option, + pub tax_amount: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub modified_at: PrimitiveDateTime, + pub authorized_attempt_id: String, + pub connector_transaction_id: Option, + pub capture_sequence: i16, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum CaptureUpdate { + ResponseUpdate { + status: storage_enums::CaptureStatus, + connector_transaction_id: Option, + }, + ErrorUpdate { + status: storage_enums::CaptureStatus, + error_code: Option, + error_message: Option, + error_reason: Option, + }, +} + +#[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] +#[diesel(table_name = captures)] +pub struct CaptureUpdateInternal { + pub status: Option, + pub error_message: Option, + pub error_code: Option, + pub error_reason: Option, + pub modified_at: Option, + pub connector_transaction_id: Option, +} + +impl CaptureUpdate { + pub fn apply_changeset(self, source: Capture) -> Capture { + let capture_update: CaptureUpdateInternal = self.into(); + Capture { + status: capture_update.status.unwrap_or(source.status), + error_message: capture_update.error_message.or(source.error_message), + error_code: capture_update.error_code.or(source.error_code), + error_reason: capture_update.error_reason.or(source.error_reason), + modified_at: common_utils::date_time::now(), + ..source + } + } +} + +impl From for CaptureUpdateInternal { + fn from(payment_attempt_child_update: CaptureUpdate) -> Self { + let now = Some(common_utils::date_time::now()); + match payment_attempt_child_update { + CaptureUpdate::ResponseUpdate { + status, + connector_transaction_id, + } => Self { + status: Some(status), + connector_transaction_id, + modified_at: now, + ..Self::default() + }, + CaptureUpdate::ErrorUpdate { + status, + error_code, + error_message, + error_reason, + } => Self { + status: Some(status), + error_code, + error_message, + error_reason, + modified_at: now, + ..Self::default() + }, + } + } +} diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index 22e226a18a..34f87e6552 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -2,10 +2,10 @@ pub mod diesel_exports { pub use super::{ DbAttemptStatus as AttemptStatus, DbAuthenticationType as AuthenticationType, - DbCaptureMethod as CaptureMethod, DbConnectorType as ConnectorType, - DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, DbDisputeStage as DisputeStage, - DbDisputeStatus as DisputeStatus, DbEventClass as EventClass, - DbEventObjectType as EventObjectType, DbEventType as EventType, + DbCaptureMethod as CaptureMethod, DbCaptureStatus as CaptureStatus, + DbConnectorType as ConnectorType, DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, + DbDisputeStage as DisputeStage, DbDisputeStatus as DisputeStatus, + DbEventClass as EventClass, DbEventObjectType as EventObjectType, DbEventType as EventType, DbFraudCheckStatus as FraudCheckStatus, DbFraudCheckType as FraudCheckType, DbFutureUsage as FutureUsage, DbIntentStatus as IntentStatus, DbMandateStatus as MandateStatus, DbMandateType as MandateType, diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs index 6f277d9094..72eba52117 100644 --- a/crates/diesel_models/src/lib.rs +++ b/crates/diesel_models/src/lib.rs @@ -1,5 +1,6 @@ pub mod address; pub mod api_keys; +pub mod capture; pub mod cards_info; pub mod configs; pub mod connector_response; diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 06e47a6fe3..6ab17c34a0 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -53,6 +53,7 @@ pub struct PaymentAttempt { // providing a location to store mandate details intermediately for transaction pub mandate_details: Option, pub error_reason: Option, + pub multiple_capture_count: Option, // reference to the payment at connector side pub connector_response_reference_id: Option, } @@ -112,6 +113,7 @@ pub struct PaymentAttemptNew { pub mandate_details: Option, pub error_reason: Option, pub connector_response_reference_id: Option, + pub multiple_capture_count: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -191,6 +193,10 @@ pub enum PaymentAttemptUpdate { error_message: Option>, error_reason: Option>, }, + MultipleCaptureUpdate { + status: Option, + multiple_capture_count: Option, + }, PreprocessingUpdate { status: storage_enums::AttemptStatus, payment_method_id: Option>, @@ -230,6 +236,7 @@ pub struct PaymentAttemptUpdateInternal { error_reason: Option>, capture_method: Option, connector_response_reference_id: Option, + multiple_capture_count: Option, } impl PaymentAttemptUpdate { @@ -437,6 +444,14 @@ impl From for PaymentAttemptUpdateInternal { connector_response_reference_id, ..Default::default() }, + PaymentAttemptUpdate::MultipleCaptureUpdate { + status, + multiple_capture_count, + } => Self { + status, + multiple_capture_count, + ..Default::default() + }, } } } diff --git a/crates/diesel_models/src/query.rs b/crates/diesel_models/src/query.rs index 42825a983e..ce24054080 100644 --- a/crates/diesel_models/src/query.rs +++ b/crates/diesel_models/src/query.rs @@ -1,5 +1,6 @@ pub mod address; pub mod api_keys; +mod capture; pub mod cards_info; pub mod configs; pub mod connector_response; diff --git a/crates/diesel_models/src/query/capture.rs b/crates/diesel_models/src/query/capture.rs new file mode 100644 index 0000000000..26623b0160 --- /dev/null +++ b/crates/diesel_models/src/query/capture.rs @@ -0,0 +1,78 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods, Table}; +use router_env::{instrument, tracing}; + +use super::generics; +use crate::{ + capture::{Capture, CaptureNew, CaptureUpdate, CaptureUpdateInternal}, + errors, + schema::captures::dsl, + PgPooledConn, StorageResult, +}; + +impl CaptureNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl Capture { + #[instrument(skip(conn))] + pub async fn find_by_capture_id(conn: &PgPooledConn, capture_id: &str) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::capture_id.eq(capture_id.to_owned()), + ) + .await + } + #[instrument(skip(conn))] + pub async fn update_with_capture_id( + self, + conn: &PgPooledConn, + capture: CaptureUpdate, + ) -> StorageResult { + match generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( + conn, + dsl::capture_id.eq(self.capture_id.to_owned()), + CaptureUpdateInternal::from(capture), + ) + .await + { + Err(error) => match error.current_context() { + errors::DatabaseError::NoFieldsToUpdate => Ok(self), + _ => Err(error), + }, + result => result, + } + } + + #[instrument(skip(conn))] + pub async fn find_all_by_merchant_id_payment_id_authorized_attempt_id( + merchant_id: &str, + payment_id: &str, + authorized_attempt_id: &str, + conn: &PgPooledConn, + ) -> StorageResult> { + generics::generic_filter::< + ::Table, + _, + <::Table as Table>::PrimaryKey, + _, + >( + conn, + dsl::authorized_attempt_id + .eq(authorized_attempt_id.to_owned()) + .and(dsl::merchant_id.eq(merchant_id.to_owned())) + .and(dsl::payment_id.eq(payment_id.to_owned())), + None, + None, + None, + ) + .await + } +} diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 9a1eeeac45..c70ad59cfd 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -53,6 +53,39 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + captures (capture_id) { + #[max_length = 64] + capture_id -> Varchar, + #[max_length = 64] + payment_id -> Varchar, + #[max_length = 64] + merchant_id -> Varchar, + status -> CaptureStatus, + amount -> Int8, + currency -> Nullable, + #[max_length = 255] + connector -> Nullable, + #[max_length = 255] + error_message -> Nullable, + #[max_length = 255] + error_code -> Nullable, + #[max_length = 255] + error_reason -> Nullable, + tax_amount -> Nullable, + created_at -> Timestamp, + modified_at -> Timestamp, + #[max_length = 64] + authorized_attempt_id -> Varchar, + #[max_length = 128] + connector_transaction_id -> Nullable, + capture_sequence -> Int2, + } +} + diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -466,6 +499,7 @@ diesel::table! { preprocessing_step_id -> Nullable, mandate_details -> Nullable, error_reason -> Nullable, + multiple_capture_count -> Nullable, #[max_length = 128] connector_response_reference_id -> Nullable, } @@ -718,6 +752,7 @@ diesel::table! { diesel::allow_tables_to_appear_in_same_query!( address, api_keys, + captures, cards_info, configs, connector_response, diff --git a/crates/router/build.rs b/crates/router/build.rs index db16158893..167ca91840 100644 --- a/crates/router/build.rs +++ b/crates/router/build.rs @@ -2,7 +2,7 @@ fn main() { // Set thread stack size to 4 MiB for debug builds // Reference: https://doc.rust-lang.org/std/thread/#stack-size #[cfg(debug_assertions)] - println!("cargo:rustc-env=RUST_MIN_STACK=4194304"); // 4 * 1024 * 1024 = 4 MiB + println!("cargo:rustc-env=RUST_MIN_STACK=6291456"); // 6 * 1024 * 1024 = 6 MiB #[cfg(feature = "vergen")] router_env::vergen::generate_cargo_instructions(); diff --git a/crates/router/src/compatibility/stripe/payment_intents/types.rs b/crates/router/src/compatibility/stripe/payment_intents/types.rs index 170e5bea59..8bc6632833 100644 --- a/crates/router/src/compatibility/stripe/payment_intents/types.rs +++ b/crates/router/src/compatibility/stripe/payment_intents/types.rs @@ -387,11 +387,12 @@ impl From for StripePaymentStatus { api_enums::IntentStatus::Succeeded => Self::Succeeded, api_enums::IntentStatus::Failed => Self::Canceled, api_enums::IntentStatus::Processing => Self::Processing, - api_enums::IntentStatus::RequiresCustomerAction => Self::RequiresAction, - api_enums::IntentStatus::RequiresMerchantAction => Self::RequiresAction, + api_enums::IntentStatus::RequiresCustomerAction + | api_enums::IntentStatus::RequiresMerchantAction => Self::RequiresAction, api_enums::IntentStatus::RequiresPaymentMethod => Self::RequiresPaymentMethod, api_enums::IntentStatus::RequiresConfirmation => Self::RequiresConfirmation, - api_enums::IntentStatus::RequiresCapture => Self::RequiresCapture, + api_enums::IntentStatus::RequiresCapture + | api_enums::IntentStatus::PartiallyCaptured => Self::RequiresCapture, api_enums::IntentStatus::Cancelled => Self::Canceled, } } diff --git a/crates/router/src/compatibility/stripe/setup_intents/types.rs b/crates/router/src/compatibility/stripe/setup_intents/types.rs index a7699323a4..896183da0d 100644 --- a/crates/router/src/compatibility/stripe/setup_intents/types.rs +++ b/crates/router/src/compatibility/stripe/setup_intents/types.rs @@ -308,7 +308,8 @@ impl From for StripeSetupStatus { api_enums::IntentStatus::RequiresMerchantAction => Self::RequiresAction, api_enums::IntentStatus::RequiresPaymentMethod => Self::RequiresPaymentMethod, api_enums::IntentStatus::RequiresConfirmation => Self::RequiresConfirmation, - api_enums::IntentStatus::RequiresCapture => { + api_enums::IntentStatus::RequiresCapture + | api_enums::IntentStatus::PartiallyCaptured => { logger::error!("Invalid status change"); Self::Canceled } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 90411677b6..9465bdf8a2 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -6,7 +6,7 @@ pub mod operations; pub mod tokenization; pub mod transformers; -use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant}; +use std::{collections::HashMap, fmt::Debug, marker::PhantomData, ops::Deref, time::Instant}; use api_models::payments::FrmMessage; use common_utils::pii; @@ -1082,6 +1082,7 @@ where pub flow: PhantomData, pub payment_intent: storage::PaymentIntent, pub payment_attempt: storage::PaymentAttempt, + pub multiple_capture_data: Option, pub connector_response: storage::ConnectorResponse, pub amount: api::Amount, pub mandate_id: Option, @@ -1108,6 +1109,96 @@ where pub frm_message: Option, } +#[derive(Clone)] +pub struct MultipleCaptureData { + previous_captures: Vec, + current_capture: storage::Capture, +} + +impl MultipleCaptureData { + fn get_previously_blocked_amount(&self) -> i64 { + self.previous_captures + .iter() + .fold(0, |accumulator, capture| { + accumulator + + match capture.status { + storage_enums::CaptureStatus::Charged + | storage_enums::CaptureStatus::Pending => capture.amount, + storage_enums::CaptureStatus::Started + | storage_enums::CaptureStatus::Failed => 0, + } + }) + } + fn get_total_blocked_amount(&self) -> i64 { + self.get_previously_blocked_amount() + + match self.current_capture.status { + api_models::enums::CaptureStatus::Charged + | api_models::enums::CaptureStatus::Pending => self.current_capture.amount, + api_models::enums::CaptureStatus::Failed + | api_models::enums::CaptureStatus::Started => 0, + } + } + fn get_previously_charged_amount(&self) -> i64 { + self.previous_captures + .iter() + .fold(0, |accumulator, capture| { + accumulator + + match capture.status { + storage_enums::CaptureStatus::Charged => capture.amount, + storage_enums::CaptureStatus::Pending + | storage_enums::CaptureStatus::Started + | storage_enums::CaptureStatus::Failed => 0, + } + }) + } + fn get_total_charged_amount(&self) -> i64 { + self.get_previously_charged_amount() + + match self.current_capture.status { + storage_enums::CaptureStatus::Charged => self.current_capture.amount, + storage_enums::CaptureStatus::Pending + | storage_enums::CaptureStatus::Started + | storage_enums::CaptureStatus::Failed => 0, + } + } + fn get_captures_count(&self) -> RouterResult { + i16::try_from(1 + self.previous_captures.len()) + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error while converting from usize to i16") + } + fn get_status_count(&self) -> HashMap { + let mut hash_map: HashMap = HashMap::new(); + hash_map.insert(storage_enums::CaptureStatus::Charged, 0); + hash_map.insert(storage_enums::CaptureStatus::Pending, 0); + hash_map.insert(storage_enums::CaptureStatus::Started, 0); + hash_map.insert(storage_enums::CaptureStatus::Failed, 0); + hash_map + .entry(self.current_capture.status) + .and_modify(|count| *count += 1); + self.previous_captures + .iter() + .fold(hash_map, |mut accumulator, capture| { + let current_capture_status = capture.status; + accumulator + .entry(current_capture_status) + .and_modify(|count| *count += 1); + accumulator + }) + } + fn get_attempt_status(&self, authorized_amount: i64) -> storage_enums::AttemptStatus { + let total_captured_amount = self.get_total_charged_amount(); + if authorized_amount == total_captured_amount { + return storage_enums::AttemptStatus::Charged; + } + let status_count_map = self.get_status_count(); + if status_count_map.get(&storage_enums::CaptureStatus::Charged) > Some(&0) { + storage_enums::AttemptStatus::PartialCharged + } else { + storage_enums::AttemptStatus::CaptureInitiated + } + } +} + #[derive(Debug, Default, Clone)] pub struct RecurringMandatePaymentData { pub payment_method_type: Option, //required for making recurring payment using saved payment method through stripe @@ -1194,6 +1285,7 @@ pub fn should_call_connector( matches!( payment_data.payment_intent.status, storage_enums::IntentStatus::RequiresCapture + | storage_enums::IntentStatus::PartiallyCaptured ) } "CompleteAuthorize" => true, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 78be0a1518..3954cc6a7e 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1345,7 +1345,7 @@ pub(crate) fn validate_capture_method( field_name: "capture_method".to_string(), current_flow: "captured".to_string(), current_value: capture_method.to_string(), - states: "manual_single, manual_multiple, scheduled".to_string() + states: "manual, manual_multiple, scheduled".to_string() })) }, ) @@ -1354,13 +1354,14 @@ pub(crate) fn validate_capture_method( #[instrument(skip_all)] pub(crate) fn validate_status(status: storage_enums::IntentStatus) -> RouterResult<()> { utils::when( - status != storage_enums::IntentStatus::RequiresCapture, + status != storage_enums::IntentStatus::RequiresCapture + && status != storage_enums::IntentStatus::PartiallyCaptured, || { Err(report!(errors::ApiErrorResponse::PaymentUnexpectedState { field_name: "payment.status".to_string(), current_flow: "captured".to_string(), current_value: status.to_string(), - states: "requires_capture".to_string() + states: "requires_capture, partially captured".to_string() })) }, ) @@ -2488,6 +2489,7 @@ pub fn get_attempt_type( } enums::IntentStatus::Cancelled | enums::IntentStatus::RequiresCapture + | enums::IntentStatus::PartiallyCaptured | enums::IntentStatus::Processing | enums::IntentStatus::Succeeded => { Err(report!(errors::ApiErrorResponse::PreconditionFailed { @@ -2577,6 +2579,7 @@ impl AttemptType { mandate_details: old_payment_attempt.mandate_details, preprocessing_step_id: None, error_reason: None, + multiple_capture_count: None, connector_response_reference_id: None, } } @@ -2697,6 +2700,7 @@ pub fn is_manual_retry_allowed( }, enums::IntentStatus::Cancelled | enums::IntentStatus::RequiresCapture + | enums::IntentStatus::PartiallyCaptured | enums::IntentStatus::Processing | enums::IntentStatus::Succeeded => Some(false), diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index e72e430285..1a6dc8ee8e 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -162,6 +162,7 @@ impl GetTracker, api::PaymentsCancelRequest> connector_customer_id: None, recurring_mandate_payment_data: None, ephemeral_key: None, + multiple_capture_data: None, redirect_response: None, frm_message: None, }, diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index 6f40bc9560..028b5c9a22 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -3,6 +3,7 @@ use std::marker::PhantomData; use api_models::enums::CancelTransaction; use async_trait::async_trait; use common_utils::ext_traits::AsyncExt; +use diesel_models::connector_response::ConnectorResponse; use error_stack::ResultExt; use router_env::{instrument, tracing}; @@ -18,7 +19,7 @@ use crate::{ types::{ api::{self, PaymentIdTypeExt}, domain, - storage::{self, enums}, + storage::{self, enums, ConnectorResponseExt, PaymentAttemptExt}, }, utils::OptionExt, }; @@ -84,20 +85,83 @@ impl GetTracker, api::PaymentsCaptu helpers::validate_capture_method(capture_method)?; + let (multiple_capture_data, connector_response) = + if capture_method == enums::CaptureMethod::ManualMultiple { + let amount_to_capture = request + .amount_to_capture + .get_required_value("amount_to_capture")?; + let previous_captures = db + .find_all_captures_by_merchant_id_payment_id_authorized_attempt_id( + &payment_attempt.merchant_id, + &payment_attempt.payment_id, + &payment_attempt.attempt_id, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + let previously_blocked_amount = + previous_captures.iter().fold(0, |accumulator, capture| { + accumulator + + match capture.status { + enums::CaptureStatus::Charged | enums::CaptureStatus::Pending => { + capture.amount + } + enums::CaptureStatus::Started | enums::CaptureStatus::Failed => 0, + } + }); + helpers::validate_amount_to_capture( + payment_attempt.amount - previously_blocked_amount, + Some(amount_to_capture), + )?; + + let capture = db + .insert_capture( + payment_attempt + .make_new_capture(amount_to_capture, enums::CaptureStatus::Started), + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::DuplicatePayment { + payment_id: payment_id.clone(), + })?; + let new_connector_response = db + .insert_connector_response( + ConnectorResponse::make_new_connector_response( + capture.payment_id.clone(), + capture.merchant_id.clone(), + capture.capture_id.clone(), + capture.connector.clone(), + ), + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::DuplicatePayment { + payment_id: payment_id.clone(), + })?; + ( + Some(payments::MultipleCaptureData { + previous_captures, + current_capture: capture, + }), + new_connector_response, + ) + } else { + let connector_response = db + .find_connector_response_by_payment_id_merchant_id_attempt_id( + &payment_attempt.payment_id, + &payment_attempt.merchant_id, + &payment_attempt.attempt_id, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + (None, connector_response) + }; + currency = payment_attempt.currency.get_required_value("currency")?; amount = payment_attempt.amount.into(); - let connector_response = db - .find_connector_response_by_payment_id_merchant_id_attempt_id( - &payment_attempt.payment_id, - &payment_attempt.merchant_id, - &payment_attempt.attempt_id, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; - let shipping_address = helpers::get_address_for_payment_request( db, None, @@ -167,6 +231,7 @@ impl GetTracker, api::PaymentsCaptu connector_customer_id: None, recurring_mandate_payment_data: None, ephemeral_key: None, + multiple_capture_data, redirect_response: None, frm_message: None, }, @@ -182,10 +247,10 @@ impl UpdateTracker, api::PaymentsCaptureRe #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - _db: &dyn StorageInterface, - payment_data: payments::PaymentData, + db: &dyn StorageInterface, + mut payment_data: payments::PaymentData, _customer: Option, - _storage_scheme: enums::MerchantStorageScheme, + storage_scheme: enums::MerchantStorageScheme, _updated_customer: Option, _mechant_key_store: &domain::MerchantKeyStore, _should_cancel_transaction: Option, @@ -196,6 +261,27 @@ impl UpdateTracker, api::PaymentsCaptureRe where F: 'b + Send, { + payment_data.payment_attempt = match &payment_data.multiple_capture_data { + Some(multiple_capture_data) => { + let mut updated_payment_attempt = db + .update_payment_attempt_with_attempt_id( + payment_data.payment_attempt, + storage::PaymentAttemptUpdate::MultipleCaptureUpdate { + status: None, + multiple_capture_count: Some( + multiple_capture_data.current_capture.capture_sequence, + ), + }, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + updated_payment_attempt.amount_to_capture = + Some(multiple_capture_data.current_capture.amount); + updated_payment_attempt + } + None => payment_data.payment_attempt, + }; Ok((Box::new(self), payment_data)) } } diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 6dc4e5a4d5..bab7c2cfc0 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -234,6 +234,7 @@ impl GetTracker, api::PaymentsRequest> for Co connector_customer_id: None, recurring_mandate_payment_data, ephemeral_key: None, + multiple_capture_data: None, redirect_response, frm_message: None, }, diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 53454f6e4d..3349e9170f 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -290,6 +290,7 @@ impl GetTracker, api::PaymentsRequest> for Pa connector_customer_id: None, recurring_mandate_payment_data, ephemeral_key: None, + multiple_capture_data: None, redirect_response: None, frm_message: None, }, diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index d5a9af98d6..810f526150 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -269,6 +269,7 @@ impl GetTracker, api::PaymentsRequest> for Pa connector_customer_id: None, recurring_mandate_payment_data, ephemeral_key, + multiple_capture_data: None, redirect_response: None, frm_message: None, }, diff --git a/crates/router/src/core/payments/operations/payment_method_validate.rs b/crates/router/src/core/payments/operations/payment_method_validate.rs index b3d7f59e2a..d0860fd27e 100644 --- a/crates/router/src/core/payments/operations/payment_method_validate.rs +++ b/crates/router/src/core/payments/operations/payment_method_validate.rs @@ -188,6 +188,7 @@ impl GetTracker, api::VerifyRequest> for Paym connector_customer_id: None, recurring_mandate_payment_data: None, ephemeral_key: None, + multiple_capture_data: None, redirect_response: None, frm_message: None, }, diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 2f03a28c29..870de1bfad 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -16,7 +16,7 @@ use crate::{ types::{ self, api, storage::{self, enums}, - transformers::ForeignInto, + transformers::{ForeignFrom, ForeignTryFrom}, }, utils, }; @@ -295,22 +295,47 @@ async fn payment_response_update_tracker( router_data: types::RouterData, storage_scheme: enums::MerchantStorageScheme, ) -> RouterResult> { - let (payment_attempt_update, connector_response_update) = match router_data.response.clone() { - Err(err) => ( - Some(storage::PaymentAttemptUpdate::ErrorUpdate { - connector: None, - status: match err.status_code { - 500..=511 => storage::enums::AttemptStatus::Pending, - _ => storage::enums::AttemptStatus::Failure, - }, - error_message: Some(Some(err.message)), - error_code: Some(Some(err.code)), - error_reason: Some(err.reason), - }), - Some(storage::ConnectorResponseUpdate::ErrorUpdate { - connector_name: Some(router_data.connector.clone()), - }), - ), + let (capture_update, mut payment_attempt_update, connector_response_update) = match router_data + .response + .clone() + { + Err(err) => { + let (capture_update, attempt_update) = match payment_data.multiple_capture_data { + Some(_) => ( + Some(storage::CaptureUpdate::ErrorUpdate { + status: match err.status_code { + 500..=511 => storage::enums::CaptureStatus::Pending, + _ => storage::enums::CaptureStatus::Failed, + }, + error_code: Some(err.code), + error_message: Some(err.message), + error_reason: err.reason, + }), + // attempt status will depend on collective capture status + None, + ), + None => ( + None, + Some(storage::PaymentAttemptUpdate::ErrorUpdate { + connector: None, + status: match err.status_code { + 500..=511 => storage::enums::AttemptStatus::Pending, + _ => storage::enums::AttemptStatus::Failure, + }, + error_message: Some(Some(err.message)), + error_code: Some(Some(err.code)), + error_reason: Some(err.reason), + }), + ), + }; + ( + capture_update, + attempt_update, + Some(storage::ConnectorResponseUpdate::ErrorUpdate { + connector_name: Some(router_data.connector.clone()), + }), + ) + } Ok(payments_response) => match payments_response { types::PaymentsResponseData::PreProcessingResponse { pre_processing_id, @@ -339,7 +364,7 @@ async fn payment_response_update_tracker( connector_response_reference_id, }; - (Some(payment_attempt_update), None) + (None, Some(payment_attempt_update), None) } types::PaymentsResponseData::TransactionResponse { resource_id, @@ -374,23 +399,38 @@ async fn payment_response_update_tracker( metrics::SUCCESSFUL_PAYMENT.add(&metrics::CONTEXT, 1, &[]); } - let payment_attempt_update = storage::PaymentAttemptUpdate::ResponseUpdate { - status: router_data.status, - connector: None, - connector_transaction_id: connector_transaction_id.clone(), - authentication_type: None, - payment_method_id: Some(router_data.payment_method_id), - mandate_id: payment_data - .mandate_id - .clone() - .map(|mandate| mandate.mandate_id), - connector_metadata, - payment_token: None, - error_code: error_status.clone(), - error_message: error_status.clone(), - error_reason: error_status, - connector_response_reference_id, - }; + let (capture_update, payment_attempt_update) = + match payment_data.multiple_capture_data { + Some(_) => ( + //if payment_data.multiple_capture_data will be Some only for multiple partial capture. + Some(storage::CaptureUpdate::ResponseUpdate { + status: enums::CaptureStatus::foreign_try_from(router_data.status)?, + connector_transaction_id: connector_transaction_id.clone(), + }), + // attempt status will depend on collective capture status + None, + ), + None => ( + None, + Some(storage::PaymentAttemptUpdate::ResponseUpdate { + status: router_data.status, + connector: None, + connector_transaction_id: connector_transaction_id.clone(), + authentication_type: None, + payment_method_id: Some(router_data.payment_method_id), + mandate_id: payment_data + .mandate_id + .clone() + .map(|mandate| mandate.mandate_id), + connector_metadata, + payment_token: None, + error_code: error_status.clone(), + error_message: error_status.clone(), + error_reason: error_status, + connector_response_reference_id, + }), + ), + }; let connector_response_update = storage::ConnectorResponseUpdate::ResponseUpdate { connector_transaction_id, @@ -400,7 +440,8 @@ async fn payment_response_update_tracker( }; ( - Some(payment_attempt_update), + capture_update, + payment_attempt_update, Some(connector_response_update), ) } @@ -415,6 +456,7 @@ async fn payment_response_update_tracker( | types::ResponseId::EncodedData(id) => Some(id), }; ( + None, Some(storage::PaymentAttemptUpdate::UnresolvedResponseUpdate { status: router_data.status, connector: None, @@ -428,13 +470,38 @@ async fn payment_response_update_tracker( None, ) } - types::PaymentsResponseData::SessionResponse { .. } => (None, None), - types::PaymentsResponseData::SessionTokenResponse { .. } => (None, None), - types::PaymentsResponseData::TokenizationResponse { .. } => (None, None), - types::PaymentsResponseData::ConnectorCustomerResponse { .. } => (None, None), - types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. } => (None, None), + types::PaymentsResponseData::SessionResponse { .. } => (None, None, None), + types::PaymentsResponseData::SessionTokenResponse { .. } => (None, None, None), + types::PaymentsResponseData::TokenizationResponse { .. } => (None, None, None), + types::PaymentsResponseData::ConnectorCustomerResponse { .. } => (None, None, None), + types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. } => (None, None, None), }, }; + payment_data.multiple_capture_data = match capture_update + .zip(payment_data.multiple_capture_data) + { + Some((capture_update, mut multiple_capture_data)) => { + let updated_capture = db + .update_capture_with_capture_id( + multiple_capture_data.current_capture, + capture_update, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + multiple_capture_data.current_capture = updated_capture; + + let authorized_amount = payment_data.payment_attempt.amount; + + payment_attempt_update = Some(storage::PaymentAttemptUpdate::MultipleCaptureUpdate { + status: Some(multiple_capture_data.get_attempt_status(authorized_amount)), + multiple_capture_count: Some(multiple_capture_data.get_captures_count()?), + }); + Some(multiple_capture_data) + } + None => None, + }; payment_data.payment_attempt = match payment_attempt_update { Some(payment_attempt_update) => db @@ -459,24 +526,18 @@ async fn payment_response_update_tracker( .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?, None => payment_data.connector_response, }; - let amount = router_data.request.get_capture_amount(); - - let amount_captured = router_data.amount_captured.or_else(|| { - if router_data.status == enums::AttemptStatus::Charged { - amount - } else { - None - } - }); + let amount_captured = get_total_amount_captured( + router_data.request, + router_data.amount_captured, + router_data.status, + &payment_data, + ); let payment_intent_update = match &router_data.response { - Err(err) => storage::PaymentIntentUpdate::PGStatusUpdate { - status: match err.status_code { - 500..=511 => enums::IntentStatus::Processing, - _ => enums::IntentStatus::Failed, - }, + Err(_) => storage::PaymentIntentUpdate::PGStatusUpdate { + status: enums::IntentStatus::foreign_from(payment_data.payment_attempt.status), }, Ok(_) => storage::PaymentIntentUpdate::ResponseUpdate { - status: router_data.status.foreign_into(), + status: enums::IntentStatus::foreign_from(payment_data.payment_attempt.status), return_url: router_data.return_url.clone(), amount_captured, }, @@ -502,3 +563,28 @@ async fn payment_response_update_tracker( Ok(payment_data) } + +fn get_total_amount_captured( + request: T, + amount_captured: Option, + router_data_status: enums::AttemptStatus, + payment_data: &PaymentData, +) -> Option { + match &payment_data.multiple_capture_data { + Some(multiple_capture_data) => { + //multiple capture + Some(multiple_capture_data.get_total_blocked_amount()) + } + None => { + //Non multiple capture + let amount = request.get_capture_amount(); + amount_captured.or_else(|| { + if router_data_status == enums::AttemptStatus::Charged { + amount + } else { + None + } + }) + } + } +} diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 88e9c6184f..f795e4b439 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -179,6 +179,7 @@ impl GetTracker, api::PaymentsSessionRequest> connector_customer_id: None, recurring_mandate_payment_data: None, ephemeral_key: None, + multiple_capture_data: None, redirect_response: None, frm_message: None, }, diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 3610926cea..03eb5294b1 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -152,6 +152,7 @@ impl GetTracker, api::PaymentsStartRequest> f connector_customer_id: None, recurring_mandate_payment_data: None, ephemeral_key: None, + multiple_capture_data: None, redirect_response: None, frm_message: None, }, diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index abf5693713..bbb9b72765 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -353,6 +353,7 @@ async fn get_tracker_for_sync< connector_customer_id: None, recurring_mandate_payment_data: None, ephemeral_key: None, + multiple_capture_data: None, redirect_response: None, frm_message, }, diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 57c8d583c8..61b9afce8b 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -338,6 +338,7 @@ impl GetTracker, api::PaymentsRequest> for Pa connector_customer_id: None, recurring_mandate_payment_data, ephemeral_key: None, + multiple_capture_data: None, redirect_response: None, frm_message: None, }, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 04a0848aba..9f34255ae1 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -943,6 +943,10 @@ impl TryFrom> for types::PaymentsCaptureD .ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?, payment_amount: payment_data.amount.into(), connector_meta: payment_data.payment_attempt.connector_metadata, + capture_method: payment_data + .payment_attempt + .capture_method + .unwrap_or_default(), }) } } diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index 00669d9ce7..7e20d1dae7 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -1,6 +1,7 @@ pub mod address; pub mod api_keys; pub mod cache; +pub mod capture; pub mod cards_info; pub mod configs; pub mod connector_response; @@ -49,6 +50,7 @@ pub trait StorageInterface: + address::AddressInterface + api_keys::ApiKeyInterface + configs::ConfigInterface + + capture::CaptureInterface + connector_response::ConnectorResponseInterface + customers::CustomerInterface + dispute::DisputeInterface @@ -122,6 +124,7 @@ pub struct MockDb { disputes: Arc>>, lockers: Arc>>, mandates: Arc>>, + captures: Arc>>, merchant_key_store: Arc>>, } @@ -147,6 +150,7 @@ impl MockDb { disputes: Default::default(), lockers: Default::default(), mandates: Default::default(), + captures: Default::default(), merchant_key_store: Default::default(), } } diff --git a/crates/router/src/db/capture.rs b/crates/router/src/db/capture.rs new file mode 100644 index 0000000000..41840e1ec3 --- /dev/null +++ b/crates/router/src/db/capture.rs @@ -0,0 +1,220 @@ +use super::MockDb; +use crate::{ + core::errors::{self, CustomResult}, + types::storage::{self as types, enums}, +}; + +#[async_trait::async_trait] +pub trait CaptureInterface { + async fn insert_capture( + &self, + capture: types::CaptureNew, + storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult; + + async fn find_all_captures_by_merchant_id_payment_id_authorized_attempt_id( + &self, + merchant_id: &str, + payment_id: &str, + authorized_attempt_id: &str, + storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult, errors::StorageError>; + + async fn update_capture_with_capture_id( + &self, + this: types::Capture, + capture: types::CaptureUpdate, + storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult; +} + +#[cfg(feature = "kv_store")] +mod storage { + use error_stack::IntoReport; + + use super::CaptureInterface; + use crate::{ + connection, + core::errors::{self, CustomResult}, + services::Store, + types::storage::{capture::*, enums}, + }; + + #[async_trait::async_trait] + impl CaptureInterface for Store { + async fn insert_capture( + &self, + capture: CaptureNew, + _storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult { + let db_call = || async { + let conn = connection::pg_connection_write(self).await?; + capture + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + }; + db_call().await + } + + async fn update_capture_with_capture_id( + &self, + this: Capture, + capture: CaptureUpdate, + _storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult { + let db_call = || async { + let conn = connection::pg_connection_write(self).await?; + this.update_with_capture_id(&conn, capture) + .await + .map_err(Into::into) + .into_report() + }; + db_call().await + } + + async fn find_all_captures_by_merchant_id_payment_id_authorized_attempt_id( + &self, + merchant_id: &str, + payment_id: &str, + authorized_attempt_id: &str, + _storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult, errors::StorageError> { + let db_call = || async { + let conn = connection::pg_connection_read(self).await?; + Capture::find_all_by_merchant_id_payment_id_authorized_attempt_id( + merchant_id, + payment_id, + authorized_attempt_id, + &conn, + ) + .await + .map_err(Into::into) + .into_report() + }; + db_call().await + } + } +} + +#[cfg(not(feature = "kv_store"))] +mod storage { + use error_stack::IntoReport; + + use super::CaptureInterface; + use crate::{ + connection, + core::errors::{self, CustomResult}, + services::Store, + types::storage::{capture::*, enums}, + }; + + #[async_trait::async_trait] + impl CaptureInterface for Store { + async fn insert_capture( + &self, + capture: CaptureNew, + _storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult { + let db_call = || async { + let conn = connection::pg_connection_write(self).await?; + capture + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + }; + db_call().await + } + + async fn update_capture_with_capture_id( + &self, + this: Capture, + capture: CaptureUpdate, + _storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult { + let db_call = || async { + let conn = connection::pg_connection_write(self).await?; + this.update_with_capture_id(&conn, capture) + .await + .map_err(Into::into) + .into_report() + }; + db_call().await + } + + async fn find_all_captures_by_merchant_id_payment_id_authorized_attempt_id( + &self, + merchant_id: &str, + payment_id: &str, + authorized_attempt_id: &str, + _storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult, errors::StorageError> { + let db_call = || async { + let conn = connection::pg_connection_read(self).await?; + Capture::find_all_by_merchant_id_payment_id_authorized_attempt_id( + merchant_id, + payment_id, + authorized_attempt_id, + &conn, + ) + .await + .map_err(Into::into) + .into_report() + }; + db_call().await + } + } +} + +#[async_trait::async_trait] +impl CaptureInterface for MockDb { + async fn insert_capture( + &self, + capture: types::CaptureNew, + _storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult { + let mut captures = self.captures.lock().await; + let capture = types::Capture { + capture_id: capture.capture_id, + payment_id: capture.payment_id, + merchant_id: capture.merchant_id, + status: capture.status, + amount: capture.amount, + currency: capture.currency, + connector: capture.connector, + error_message: capture.error_message, + error_code: capture.error_code, + error_reason: capture.error_reason, + tax_amount: capture.tax_amount, + created_at: capture.created_at, + modified_at: capture.modified_at, + authorized_attempt_id: capture.authorized_attempt_id, + capture_sequence: capture.capture_sequence, + connector_transaction_id: capture.connector_transaction_id, + }; + captures.push(capture.clone()); + Ok(capture) + } + + async fn update_capture_with_capture_id( + &self, + _this: types::Capture, + _capture: types::CaptureUpdate, + _storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult { + //Implement function for `MockDb` + Err(errors::StorageError::MockDbError)? + } + async fn find_all_captures_by_merchant_id_payment_id_authorized_attempt_id( + &self, + _merchant_id: &str, + _payment_id: &str, + _authorized_attempt_id: &str, + _storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult, errors::StorageError> { + //Implement function for `MockDb` + Err(errors::StorageError::MockDbError)? + } +} diff --git a/crates/router/src/db/payment_attempt.rs b/crates/router/src/db/payment_attempt.rs index 702463aba7..7230fa7894 100644 --- a/crates/router/src/db/payment_attempt.rs +++ b/crates/router/src/db/payment_attempt.rs @@ -365,6 +365,7 @@ impl PaymentAttemptInterface for MockDb { mandate_details: payment_attempt.mandate_details, preprocessing_step_id: payment_attempt.preprocessing_step_id, error_reason: payment_attempt.error_reason, + multiple_capture_count: payment_attempt.multiple_capture_count, connector_response_reference_id: None, }; payment_attempts.push(payment_attempt.clone()); @@ -504,6 +505,7 @@ mod storage { mandate_details: payment_attempt.mandate_details.clone(), preprocessing_step_id: payment_attempt.preprocessing_step_id.clone(), error_reason: payment_attempt.error_reason.clone(), + multiple_capture_count: payment_attempt.multiple_capture_count, connector_response_reference_id: None, }; diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index cdb9ebd6e5..9c1e8b0b6d 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -330,6 +330,7 @@ pub struct PaymentsCaptureData { pub currency: storage_enums::Currency, pub connector_transaction_id: String, pub payment_amount: i64, + pub capture_method: storage_enums::CaptureMethod, pub connector_meta: Option, } diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index 0b66ac7d8a..46e3eac5fb 100644 --- a/crates/router/src/types/storage.rs +++ b/crates/router/src/types/storage.rs @@ -1,5 +1,6 @@ pub mod address; pub mod api_keys; +pub mod capture; pub mod cards_info; pub mod configs; pub mod connector_response; @@ -27,8 +28,8 @@ pub mod refund; pub mod reverse_lookup; pub use self::{ - address::*, api_keys::*, cards_info::*, configs::*, connector_response::*, customers::*, - dispute::*, ephemeral_key::*, events::*, file::*, locker_mock_up::*, mandate::*, + address::*, api_keys::*, capture::*, cards_info::*, configs::*, connector_response::*, + customers::*, dispute::*, ephemeral_key::*, events::*, file::*, locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*, merchant_key_store::*, payment_attempt::*, payment_intent::*, payment_method::*, payout_attempt::*, payouts::*, process_tracker::*, refund::*, reverse_lookup::*, diff --git a/crates/router/src/types/storage/capture.rs b/crates/router/src/types/storage/capture.rs new file mode 100644 index 0000000000..7e093e9a74 --- /dev/null +++ b/crates/router/src/types/storage/capture.rs @@ -0,0 +1 @@ +pub use diesel_models::capture::*; diff --git a/crates/router/src/types/storage/connector_response.rs b/crates/router/src/types/storage/connector_response.rs index e5780c3c06..1017d406a9 100644 --- a/crates/router/src/types/storage/connector_response.rs +++ b/crates/router/src/types/storage/connector_response.rs @@ -2,3 +2,34 @@ pub use diesel_models::connector_response::{ ConnectorResponse, ConnectorResponseNew, ConnectorResponseUpdate, ConnectorResponseUpdateInternal, }; + +pub trait ConnectorResponseExt { + fn make_new_connector_response( + payment_id: String, + merchant_id: String, + attempt_id: String, + connector: Option, + ) -> ConnectorResponseNew; +} + +impl ConnectorResponseExt for ConnectorResponse { + fn make_new_connector_response( + payment_id: String, + merchant_id: String, + attempt_id: String, + connector: Option, + ) -> ConnectorResponseNew { + let now = common_utils::date_time::now(); + ConnectorResponseNew { + payment_id, + merchant_id, + attempt_id, + created_at: now, + modified_at: now, + connector_name: connector, + connector_transaction_id: None, + authentication_data: None, + encoded_data: None, + } + } +} diff --git a/crates/router/src/types/storage/payment_attempt.rs b/crates/router/src/types/storage/payment_attempt.rs index 4bc7d3d110..76134ee8cb 100644 --- a/crates/router/src/types/storage/payment_attempt.rs +++ b/crates/router/src/types/storage/payment_attempt.rs @@ -1,6 +1,7 @@ pub use diesel_models::payment_attempt::{ PaymentAttempt, PaymentAttemptNew, PaymentAttemptUpdate, PaymentAttemptUpdateInternal, }; +use diesel_models::{capture::CaptureNew, enums}; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct RoutingData { @@ -11,6 +12,49 @@ pub struct RoutingData { #[cfg(feature = "kv_store")] impl crate::utils::storage_partitioning::KvStorePartition for PaymentAttempt {} +pub trait PaymentAttemptExt { + fn make_new_capture( + &self, + capture_amount: i64, + capture_status: enums::CaptureStatus, + ) -> CaptureNew; + + fn get_next_capture_id(&self) -> String; +} + +impl PaymentAttemptExt for PaymentAttempt { + fn make_new_capture( + &self, + capture_amount: i64, + capture_status: enums::CaptureStatus, + ) -> CaptureNew { + let capture_sequence = self.multiple_capture_count.unwrap_or_default() + 1; + let now = common_utils::date_time::now(); + CaptureNew { + payment_id: self.payment_id.clone(), + merchant_id: self.merchant_id.clone(), + capture_id: self.get_next_capture_id(), + status: capture_status, + amount: capture_amount, + currency: self.currency, + connector: self.connector.clone(), + error_message: None, + tax_amount: None, + created_at: now, + modified_at: now, + error_code: None, + error_reason: None, + authorized_attempt_id: self.attempt_id.clone(), + capture_sequence, + connector_transaction_id: None, + } + } + fn get_next_capture_id(&self) -> String { + let next_sequence_number = self.multiple_capture_count.unwrap_or_default() + 1; + format!("{}_{}", self.attempt_id.clone(), next_sequence_number) + } +} + #[cfg(test)] #[cfg(feature = "dummy_connector")] mod tests { diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 16eac03ac1..80574fe946 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -76,8 +76,8 @@ impl ForeignFrom for storage_enums::IntentStatus { } storage_enums::AttemptStatus::Unresolved => Self::RequiresMerchantAction, - storage_enums::AttemptStatus::PartialCharged - | storage_enums::AttemptStatus::Started + storage_enums::AttemptStatus::PartialCharged => Self::PartiallyCaptured, + storage_enums::AttemptStatus::Started | storage_enums::AttemptStatus::AuthenticationSuccessful | storage_enums::AttemptStatus::Authorizing | storage_enums::AttemptStatus::CodInitiated @@ -96,6 +96,45 @@ impl ForeignFrom for storage_enums::IntentStatus { } } +impl ForeignTryFrom for storage_enums::CaptureStatus { + type Error = error_stack::Report; + + fn foreign_try_from( + attempt_status: storage_enums::AttemptStatus, + ) -> errors::RouterResult { + match attempt_status { + storage_enums::AttemptStatus::Charged + | storage_enums::AttemptStatus::PartialCharged => Ok(Self::Charged), + storage_enums::AttemptStatus::Pending + | storage_enums::AttemptStatus::CaptureInitiated => Ok(Self::Pending), + storage_enums::AttemptStatus::Failure + | storage_enums::AttemptStatus::CaptureFailed => Ok(Self::Failed), + + storage_enums::AttemptStatus::Started + | storage_enums::AttemptStatus::AuthenticationFailed + | storage_enums::AttemptStatus::RouterDeclined + | storage_enums::AttemptStatus::AuthenticationPending + | storage_enums::AttemptStatus::AuthenticationSuccessful + | storage_enums::AttemptStatus::Authorized + | storage_enums::AttemptStatus::AuthorizationFailed + | storage_enums::AttemptStatus::Authorizing + | storage_enums::AttemptStatus::CodInitiated + | storage_enums::AttemptStatus::Voided + | storage_enums::AttemptStatus::VoidInitiated + | storage_enums::AttemptStatus::VoidFailed + | storage_enums::AttemptStatus::AutoRefunded + | storage_enums::AttemptStatus::Unresolved + | storage_enums::AttemptStatus::PaymentMethodAwaited + | storage_enums::AttemptStatus::ConfirmationAwaited + | storage_enums::AttemptStatus::DeviceDataCollectionPending => { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "AttemptStatus must be one of these for multiple partial captures [Charged, PartialCharged, Pending, CaptureInitiated, Failure, CaptureFailed]".into(), + }.into()) + } + } + } +} + impl ForeignFrom for storage_enums::MandateDataType { fn foreign_from(from: api_models::payments::MandateType) -> Self { match from { diff --git a/migrations/2023-07-07-091223_create_captures_table/down.sql b/migrations/2023-07-07-091223_create_captures_table/down.sql new file mode 100644 index 0000000000..b0795cc2ce --- /dev/null +++ b/migrations/2023-07-07-091223_create_captures_table/down.sql @@ -0,0 +1,15 @@ + +DROP INDEX captures_merchant_id_payment_id_authorized_attempt_id_index; +DROP INDEX captures_connector_transaction_id_index; + +DROP TABLE captures; +DROP TYPE "CaptureStatus"; + +DELETE FROM pg_enum +WHERE enumlabel = 'partially_captured' +AND enumtypid = ( + SELECT oid FROM pg_type WHERE typname = 'IntentStatus' +); + +ALTER TABLE payment_attempt +DROP COLUMN multiple_capture_count; \ No newline at end of file diff --git a/migrations/2023-07-07-091223_create_captures_table/up.sql b/migrations/2023-07-07-091223_create_captures_table/up.sql new file mode 100644 index 0000000000..d477931a2b --- /dev/null +++ b/migrations/2023-07-07-091223_create_captures_table/up.sql @@ -0,0 +1,38 @@ + +CREATE TYPE "CaptureStatus" AS ENUM ( + 'started', + 'charged', + 'pending', + 'failed' +); +ALTER TYPE "IntentStatus" ADD VALUE If NOT EXISTS 'partially_captured' AFTER 'requires_capture'; +CREATE TABLE captures( + capture_id VARCHAR(64) NOT NULL PRIMARY KEY, + payment_id VARCHAR(64) NOT NULL, + merchant_id VARCHAR(64) NOT NULL, + status "CaptureStatus" NOT NULL, + amount BIGINT NOT NULL, + currency "Currency", + connector VARCHAR(255), + error_message VARCHAR(255), + error_code VARCHAR(255), + error_reason VARCHAR(255), + tax_amount BIGINT, + created_at TIMESTAMP NOT NULL, + modified_at TIMESTAMP NOT NULL, + authorized_attempt_id VARCHAR(64) NOT NULL, + connector_transaction_id VARCHAR(128), + capture_sequence SMALLINT NOT NULL +); + +CREATE INDEX captures_merchant_id_payment_id_authorized_attempt_id_index ON captures ( + merchant_id, + payment_id, + authorized_attempt_id +); +CREATE INDEX captures_connector_transaction_id_index ON captures ( + connector_transaction_id +); + +ALTER TABLE payment_attempt +ADD COLUMN multiple_capture_count SMALLINT; --number of captures available for this payment attempt in captures table diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 948ed7518a..ac28e9563d 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -5484,7 +5484,8 @@ "requires_merchant_action", "requires_payment_method", "requires_confirmation", - "requires_capture" + "requires_capture", + "partially_captured" ] }, "KakaoPayRedirection": {