diff --git a/api-reference/v1/openapi_spec_v1.json b/api-reference/v1/openapi_spec_v1.json index 1097811dfd..2182ecbef4 100644 --- a/api-reference/v1/openapi_spec_v1.json +++ b/api-reference/v1/openapi_spec_v1.json @@ -11831,6 +11831,7 @@ "paypal_test", "aci", "adyen", + "affirm", "airwallex", "archipel", "authorizedotnet", @@ -29839,6 +29840,7 @@ "paypal_test", "aci", "adyen", + "affirm", "airwallex", "archipel", "authorizedotnet", diff --git a/api-reference/v2/openapi_spec_v2.json b/api-reference/v2/openapi_spec_v2.json index 5bf813777d..2d9164f8cb 100644 --- a/api-reference/v2/openapi_spec_v2.json +++ b/api-reference/v2/openapi_spec_v2.json @@ -8375,6 +8375,7 @@ "paypal_test", "aci", "adyen", + "affirm", "airwallex", "archipel", "authorizedotnet", @@ -23493,6 +23494,7 @@ "paypal_test", "aci", "adyen", + "affirm", "airwallex", "archipel", "authorizedotnet", diff --git a/config/config.example.toml b/config/config.example.toml index ab7f0ebdef..00f7ba33a5 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -604,6 +604,9 @@ seicomart = { country = "JP", currency = "JPY" } pay_easy = { country = "JP", currency = "JPY" } boleto = { country = "BR", currency = "BRL" } +[pm_filters.affirm] +affirm = { country = "CA,US", currency = "CAD,USD" } + [pm_filters.airwallex] credit = { country = "AU,HK,SG,NZ,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } debit = { country = "AU,HK,SG,NZ,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index a8c1fcfd66..40a0bdb137 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -332,6 +332,9 @@ seven_eleven = { country = "JP", currency = "JPY" } sofort = { country = "AT,BE,DE,ES,CH,NL", currency = "CHF,EUR"} paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +[pm_filters.affirm] +affirm = { country = "CA,US", currency = "CAD,USD" } + swish = { country = "SE", currency = "SEK" } touch_n_go = { country = "MY", currency = "MYR" } trustly = { country = "ES,GB,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 1900a06ff7..c4c37289fa 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -354,6 +354,9 @@ vipps = { country = "NO", currency = "NOK" } walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } +[pm_filters.affirm] +affirm = { country = "CA,US", currency = "CAD,USD" } + [pm_filters.airwallex] credit = { country = "AU,HK,SG,NZ,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } debit = { country = "AU,HK,SG,NZ,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 3c7070879e..171b4bf098 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -363,6 +363,9 @@ walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } pix = { country = "BR", currency = "BRL" } +[pm_filters.affirm] +affirm = { country = "CA,US", currency = "CAD,USD" } + [pm_filters.airwallex] credit = { country = "AU,HK,SG,NZ,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } debit = { country = "AU,HK,SG,NZ,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } diff --git a/config/development.toml b/config/development.toml index 9380fa7e6e..a2d06e7d28 100644 --- a/config/development.toml +++ b/config/development.toml @@ -548,6 +548,9 @@ pay_easy = { country = "JP", currency = "JPY" } pix = { country = "BR", currency = "BRL" } boleto = { country = "BR", currency = "BRL" } +[pm_filters.affirm] +affirm = { country = "CA,US", currency = "CAD,USD" } + [pm_filters.airwallex] credit = { country = "AU,HK,SG,NZ,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } debit = { country = "AU,HK,SG,NZ,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL" } diff --git a/config/docker_compose.toml b/config/docker_compose.toml index dcf86bbcfa..544caf4c9f 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -497,6 +497,9 @@ seven_eleven = { country = "JP", currency = "JPY" } sofort = { country = "AT,BE,DE,ES,CH,NL", currency = "CHF,EUR"} paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +[pm_filters.affirm] +affirm = { country = "CA,US", currency = "CAD,USD" } + swish = { country = "SE", currency = "SEK" } touch_n_go = { country = "MY", currency = "MYR" } trustly = { country = "ES,GB,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } diff --git a/crates/common_enums/src/connector_enums.rs b/crates/common_enums/src/connector_enums.rs index 9d73bc6bc9..c34d0cdf31 100644 --- a/crates/common_enums/src/connector_enums.rs +++ b/crates/common_enums/src/connector_enums.rs @@ -61,6 +61,7 @@ pub enum RoutableConnectors { DummyConnector7, Aci, Adyen, + Affirm, Airwallex, // Amazonpay, Archipel, @@ -229,6 +230,7 @@ pub enum Connector { DummyConnector7, Aci, Adyen, + Affirm, Airwallex, // Amazonpay, Archipel, @@ -427,6 +429,7 @@ impl Connector { // Add Separate authentication support for connectors | Self::Authipay | Self::Adyen + | Self::Affirm | Self::Adyenplatform | Self::Airwallex // | Self::Amazonpay @@ -601,6 +604,7 @@ impl From for Connector { RoutableConnectors::DummyConnector7 => Self::DummyConnector7, RoutableConnectors::Aci => Self::Aci, RoutableConnectors::Adyen => Self::Adyen, + RoutableConnectors::Affirm => Self::Affirm, RoutableConnectors::Airwallex => Self::Airwallex, RoutableConnectors::Archipel => Self::Archipel, RoutableConnectors::Authorizedotnet => Self::Authorizedotnet, @@ -732,6 +736,7 @@ impl TryFrom for RoutableConnectors { Connector::DummyConnector7 => Ok(Self::DummyConnector7), Connector::Aci => Ok(Self::Aci), Connector::Adyen => Ok(Self::Adyen), + Connector::Affirm => Ok(Self::Affirm), Connector::Airwallex => Ok(Self::Airwallex), Connector::Archipel => Ok(Self::Archipel), Connector::Authorizedotnet => Ok(Self::Authorizedotnet), diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index 7a77a24198..c1cae71ffe 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -190,6 +190,7 @@ pub struct ConnectorConfig { pub katapult: Option, pub aci: Option, pub adyen: Option, + pub affirm: Option, #[cfg(feature = "payouts")] pub adyen_payout: Option, #[cfg(feature = "payouts")] @@ -398,6 +399,7 @@ impl ConnectorConfig { Connector::Aci => Ok(connector_data.aci), Connector::Authipay => Ok(connector_data.authipay), Connector::Adyen => Ok(connector_data.adyen), + Connector::Affirm => Ok(connector_data.affirm), Connector::Adyenplatform => Err("Use get_payout_connector_config".to_string()), Connector::Airwallex => Ok(connector_data.airwallex), Connector::Archipel => Ok(connector_data.archipel), diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index 5904ee6d73..bdedcf9d1f 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -6621,8 +6621,11 @@ key1="Merchant Acceptor Key" merchant_secret="Source verification key" [affirm] -[affirm.connector_auth.HeaderKey] +[[affirm.pay_later]] + payment_method_type = "affirm" +[affirm.connector_auth.BodyKey] api_key = "API Key" +key1 = "API Secret" [trustpayments] [trustpayments.connector_auth.HeaderKey] diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index c3d6fe5ce7..21c9f20fed 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -5275,8 +5275,11 @@ key1="Merchant Acceptor Key" merchant_secret="Source verification key" [affirm] -[affirm.connector_auth.HeaderKey] +[[affirm.pay_later]] + payment_method_type = "affirm" +[affirm.connector_auth.BodyKey] api_key = "API Key" +key1 = "API Secret" [trustpayments] [trustpayments.connector_auth.HeaderKey] diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index 77cbac84a0..597c3fb727 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -6604,8 +6604,11 @@ key1="Merchant Acceptor Key" merchant_secret="Source verification key" [affirm] -[affirm.connector_auth.HeaderKey] +[[affirm.pay_later]] + payment_method_type = "affirm" +[affirm.connector_auth.BodyKey] api_key = "API Key" +key1 = "API Secret" [trustpayments] [trustpayments.connector_auth.HeaderKey] diff --git a/crates/hyperswitch_connectors/src/connectors/affirm.rs b/crates/hyperswitch_connectors/src/connectors/affirm.rs index 30735dcfc6..9d1f0ed4e8 100644 --- a/crates/hyperswitch_connectors/src/connectors/affirm.rs +++ b/crates/hyperswitch_connectors/src/connectors/affirm.rs @@ -2,12 +2,14 @@ pub mod transformers; use std::sync::LazyLock; +use base64::Engine; use common_enums::enums; use common_utils::{ + consts::BASE64_ENGINE, errors::CustomResult, ext_traits::BytesExt, request::{Method, Request, RequestBuilder, RequestContent}, - types::{AmountConvertor, StringMinorUnit, StringMinorUnitForConnector}, + types::{AmountConvertor, MinorUnit, MinorUnitForConnector}, }; use error_stack::{report, ResultExt}; use hyperswitch_domain_models::{ @@ -15,20 +17,25 @@ use hyperswitch_domain_models::{ router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, router_flow_types::{ access_token_auth::AccessTokenAuth, - payments::{Authorize, Capture, PSync, PaymentMethodToken, Session, SetupMandate, Void}, + payments::{ + Authorize, Capture, CompleteAuthorize, PSync, PaymentMethodToken, Session, + SetupMandate, Void, + }, refunds::{Execute, RSync}, }, router_request_types::{ - AccessTokenRequestData, PaymentMethodTokenizationData, PaymentsAuthorizeData, - PaymentsCancelData, PaymentsCaptureData, PaymentsSessionData, PaymentsSyncData, - RefundsData, SetupMandateRequestData, + AccessTokenRequestData, CompleteAuthorizeData, PaymentMethodTokenizationData, + PaymentsAuthorizeData, PaymentsCancelData, PaymentsCaptureData, PaymentsSessionData, + PaymentsSyncData, RefundsData, SetupMandateRequestData, }, router_response_types::{ - ConnectorInfo, PaymentsResponseData, RefundsResponseData, SupportedPaymentMethods, + ConnectorInfo, PaymentMethodDetails, PaymentsResponseData, RefundsResponseData, + SupportedPaymentMethods, SupportedPaymentMethodsExt, }, types::{ - PaymentsAuthorizeRouterData, PaymentsCaptureRouterData, PaymentsSyncRouterData, - RefundSyncRouterData, RefundsRouterData, + PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData, + PaymentsCompleteAuthorizeRouterData, PaymentsSyncRouterData, RefundSyncRouterData, + RefundsRouterData, }, }; use hyperswitch_interfaces::{ @@ -42,20 +49,20 @@ use hyperswitch_interfaces::{ types::{self, Response}, webhooks, }; -use masking::{ExposeInterface, Mask}; +use masking::{Mask, PeekInterface}; use transformers as affirm; use crate::{constants::headers, types::ResponseRouterData, utils}; #[derive(Clone)] pub struct Affirm { - amount_converter: &'static (dyn AmountConvertor + Sync), + amount_converter: &'static (dyn AmountConvertor + Sync), } impl Affirm { pub fn new() -> &'static Self { &Self { - amount_converter: &StringMinorUnitForConnector, + amount_converter: &MinorUnitForConnector, } } } @@ -65,6 +72,7 @@ impl api::PaymentSession for Affirm {} impl api::ConnectorAccessToken for Affirm {} impl api::MandateSetup for Affirm {} impl api::PaymentAuthorize for Affirm {} +impl api::PaymentsCompleteAuthorize for Affirm {} impl api::PaymentSync for Affirm {} impl api::PaymentCapture for Affirm {} impl api::PaymentVoid for Affirm {} @@ -105,9 +113,6 @@ impl ConnectorCommon for Affirm { fn get_currency_unit(&self) -> api::CurrencyUnit { api::CurrencyUnit::Minor - // TODO! Check connector documentation, on which unit they are processing the currency. - // If the connector accepts amount in lower unit ( i.e cents for USD) then return api::CurrencyUnit::Minor, - // if connector accepts amount in base unit (i.e dollars for USD) then return api::CurrencyUnit::Base } fn common_get_content_type(&self) -> &'static str { @@ -124,9 +129,15 @@ impl ConnectorCommon for Affirm { ) -> CustomResult)>, errors::ConnectorError> { let auth = affirm::AffirmAuthType::try_from(auth_type) .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let encoded_api_key = BASE64_ENGINE.encode(format!( + "{}:{}", + auth.public_key.peek(), + auth.private_key.peek() + )); + Ok(vec![( headers::AUTHORIZATION.to_string(), - auth.api_key.expose().into_masked(), + format!("Basic {encoded_api_key}").into_masked(), )]) } @@ -144,10 +155,10 @@ impl ConnectorCommon for Affirm { router_env::logger::info!(connector_response=?response); Ok(ErrorResponse { - status_code: res.status_code, + status_code: response.status_code, code: response.code, message: response.message, - reason: response.reason, + reason: Some(response.error_type), attempt_status: None, connector_transaction_id: None, network_advice_code: None, @@ -208,9 +219,11 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let endpoint = self.base_url(connectors); + + Ok(format!("{endpoint}/v2/checkout/direct")) } fn get_request_body( @@ -257,7 +270,7 @@ impl ConnectorIntegration, res: Response, ) -> CustomResult { - let response: affirm::AffirmPaymentsResponse = res + let response: affirm::AffirmResponseWrapper = res .response .parse_struct("Affirm PaymentsAuthorizeResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -279,6 +292,90 @@ impl ConnectorIntegration + for Affirm +{ + fn get_headers( + &self, + req: &PaymentsCompleteAuthorizeRouterData, + connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &PaymentsCompleteAuthorizeRouterData, + connectors: &Connectors, + ) -> CustomResult { + let endpoint = self.base_url(connectors); + + Ok(format!("{endpoint}/v1/transactions")) + } + + fn get_request_body( + &self, + req: &PaymentsCompleteAuthorizeRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let connector_req = affirm::AffirmCompleteAuthorizeRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &PaymentsCompleteAuthorizeRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&types::PaymentsCompleteAuthorizeType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsCompleteAuthorizeType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &PaymentsCompleteAuthorizeRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: affirm::AffirmCompleteAuthorizeResponse = res + .response + .parse_struct("Affirm PaymentsCompleteAuthorizeResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + impl ConnectorIntegration for Affirm { fn get_headers( &self, @@ -294,10 +391,17 @@ impl ConnectorIntegration for Aff fn get_url( &self, - _req: &PaymentsSyncRouterData, - _connectors: &Connectors, + req: &PaymentsSyncRouterData, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let endpoint = self.base_url(connectors); + let transaction_id = req + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + + Ok(format!("{endpoint}/v1/transactions/{transaction_id}",)) } fn build_request( @@ -321,7 +425,7 @@ impl ConnectorIntegration for Aff event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult { - let response: affirm::AffirmPaymentsResponse = res + let response: affirm::AffirmResponseWrapper = res .response .parse_struct("affirm PaymentsSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -358,18 +462,31 @@ impl ConnectorIntegration fo fn get_url( &self, - _req: &PaymentsCaptureRouterData, - _connectors: &Connectors, + req: &PaymentsCaptureRouterData, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let endpoint = self.base_url(connectors); + let transaction_id = req.request.connector_transaction_id.clone(); + + Ok(format!( + "{endpoint}/v1/transactions/{transaction_id}/capture" + )) } fn get_request_body( &self, - _req: &PaymentsCaptureRouterData, + req: &PaymentsCaptureRouterData, _connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + let amount = utils::convert_amount( + self.amount_converter, + req.request.minor_amount_to_capture, + req.request.currency, + )?; + + let connector_router_data = affirm::AffirmRouterData::from((amount, req)); + let connector_req = affirm::AffirmCaptureRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -398,7 +515,7 @@ impl ConnectorIntegration fo event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult { - let response: affirm::AffirmPaymentsResponse = res + let response: affirm::AffirmCaptureResponse = res .response .parse_struct("Affirm PaymentsCaptureResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -420,7 +537,86 @@ impl ConnectorIntegration fo } } -impl ConnectorIntegration for Affirm {} +impl ConnectorIntegration for Affirm { + fn get_headers( + &self, + req: &PaymentsCancelRouterData, + connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &PaymentsCancelRouterData, + connectors: &Connectors, + ) -> CustomResult { + let endpoint = self.base_url(connectors); + let transaction_id = req.request.connector_transaction_id.clone(); + + Ok(format!("{endpoint}/v1/transactions/{transaction_id}/void")) + } + + fn get_request_body( + &self, + req: &PaymentsCancelRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let connector_req = affirm::AffirmCancelRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &PaymentsCancelRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) + .set_body(types::PaymentsVoidType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &PaymentsCancelRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: affirm::AffirmCancelResponse = res + .response + .parse_struct("GetnetPaymentsVoidResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} impl ConnectorIntegration for Affirm { fn get_headers( @@ -437,10 +633,15 @@ impl ConnectorIntegration for Affirm fn get_url( &self, - _req: &RefundsRouterData, - _connectors: &Connectors, + req: &RefundsRouterData, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let endpoint = self.base_url(connectors); + let transaction_id = req.request.connector_transaction_id.clone(); + + Ok(format!( + "{endpoint}/v1/transactions/{transaction_id}/refund" + )) } fn get_request_body( @@ -484,10 +685,10 @@ impl ConnectorIntegration for Affirm event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult, errors::ConnectorError> { - let response: affirm::RefundResponse = - res.response - .parse_struct("affirm RefundResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let response: affirm::AffirmRefundResponse = res + .response + .parse_struct("affirm RefundResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); RouterData::try_from(ResponseRouterData { @@ -521,10 +722,13 @@ impl ConnectorIntegration for Affirm { fn get_url( &self, - _req: &RefundSyncRouterData, - _connectors: &Connectors, + req: &RefundSyncRouterData, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let endpoint = self.base_url(connectors); + let transaction_id = req.request.connector_transaction_id.clone(); + + Ok(format!("{endpoint}/v1/transactions/{transaction_id}")) } fn build_request( @@ -538,9 +742,6 @@ impl ConnectorIntegration for Affirm { .url(&types::RefundSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::RefundSyncType::get_headers(self, req, connectors)?) - .set_body(types::RefundSyncType::get_request_body( - self, req, connectors, - )?) .build(), )) } @@ -551,7 +752,7 @@ impl ConnectorIntegration for Affirm { event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult { - let response: affirm::RefundResponse = res + let response: affirm::AffirmRsyncResponse = res .response .parse_struct("affirm RefundSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -597,12 +798,31 @@ impl webhooks::IncomingWebhook for Affirm { } } -static AFFIRM_SUPPORTED_PAYMENT_METHODS: LazyLock = - LazyLock::new(SupportedPaymentMethods::new); +static AFFIRM_SUPPORTED_PAYMENT_METHODS: LazyLock = LazyLock::new(|| { + let supported_capture_methods = vec![ + enums::CaptureMethod::Automatic, + enums::CaptureMethod::Manual, + ]; + + let mut affirm_supported_payment_methods = SupportedPaymentMethods::new(); + + affirm_supported_payment_methods.add( + enums::PaymentMethod::PayLater, + enums::PaymentMethodType::Affirm, + PaymentMethodDetails { + mandates: enums::FeatureStatus::NotSupported, + refunds: enums::FeatureStatus::Supported, + supported_capture_methods, + specific_features: None, + }, + ); + + affirm_supported_payment_methods +}); static AFFIRM_CONNECTOR_INFO: ConnectorInfo = ConnectorInfo { display_name: "Affirm", - description: "Affirm connector", + description: "Affirm connector is a payment gateway integration that processes Affirm’s buy now, pay later financing by managing payment authorization, capture, refunds, and transaction sync via Affirm’s API.", connector_type: enums::HyperswitchConnectorCategory::PaymentGateway, integration_status: enums::ConnectorIntegrationStatus::Alpha, }; diff --git a/crates/hyperswitch_connectors/src/connectors/affirm/transformers.rs b/crates/hyperswitch_connectors/src/connectors/affirm/transformers.rs index 4b606e04bb..ca10c27e34 100644 --- a/crates/hyperswitch_connectors/src/connectors/affirm/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/affirm/transformers.rs @@ -1,28 +1,33 @@ -use common_enums::enums; -use common_utils::types::StringMinorUnit; +use common_enums::{enums, CountryAlpha2, Currency}; +use common_utils::{pii, request::Method, types::MinorUnit}; +use error_stack::{report, ResultExt}; use hyperswitch_domain_models::{ - payment_method_data::PaymentMethodData, + payment_method_data::{PayLaterData, PaymentMethodData}, router_data::{ConnectorAuthType, RouterData}, router_flow_types::refunds::{Execute, RSync}, - router_request_types::ResponseId, - router_response_types::{PaymentsResponseData, RefundsResponseData}, - types::{PaymentsAuthorizeRouterData, RefundsRouterData}, + router_request_types::{PaymentsCancelData, PaymentsCaptureData, ResponseId}, + router_response_types::{PaymentsResponseData, RedirectForm, RefundsResponseData}, + types::{ + PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData, + PaymentsCompleteAuthorizeRouterData, RefundsRouterData, + }, }; use hyperswitch_interfaces::errors; use masking::Secret; use serde::{Deserialize, Serialize}; +use serde_json::Value; -use crate::types::{RefundsResponseRouterData, ResponseRouterData}; - -//TODO: Fill the struct with respective fields +use crate::{ + types::{RefundsResponseRouterData, ResponseRouterData}, + utils::{PaymentsAuthorizeRequestData, RouterData as OtherRouterData}, +}; pub struct AffirmRouterData { - pub amount: StringMinorUnit, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub amount: MinorUnit, pub router_data: T, } -impl From<(StringMinorUnit, T)> for AffirmRouterData { - fn from((amount, item): (StringMinorUnit, T)) -> Self { - //Todo : use utils to convert the amount to the type of amount that a connector accepts +impl From<(MinorUnit, T)> for AffirmRouterData { + fn from((amount, item): (MinorUnit, T)) -> Self { Self { amount, router_data: item, @@ -30,88 +35,481 @@ impl From<(StringMinorUnit, T)> for AffirmRouterData { } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize)] pub struct AffirmPaymentsRequest { - amount: StringMinorUnit, - card: AffirmCard, + pub merchant: Merchant, + pub items: Vec, + pub shipping: Option, + pub billing: Option, + pub total: MinorUnit, + pub currency: Currency, + pub order_id: Option, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct AffirmCard { - number: cards::CardNumber, - expiry_month: Secret, - expiry_year: Secret, - cvc: Secret, - complete: bool, +#[derive(Debug, Serialize)] +pub struct AffirmCompleteAuthorizeRequest { + pub order_id: Option, + pub reference_id: Option, + pub transaction_id: String, +} + +impl TryFrom<&PaymentsCompleteAuthorizeRouterData> for AffirmCompleteAuthorizeRequest { + type Error = error_stack::Report; + fn try_from(item: &PaymentsCompleteAuthorizeRouterData) -> Result { + let transaction_id = item.request.connector_transaction_id.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "connector_transaction_id", + }, + )?; + + let reference_id = item.reference_id.clone(); + let order_id = item.connector_request_reference_id.clone(); + Ok(Self { + transaction_id, + order_id: Some(order_id), + reference_id, + }) + } +} + +#[derive(Debug, Serialize)] +pub struct Merchant { + pub public_api_key: Secret, + pub user_confirmation_url: String, + pub user_cancel_url: String, + pub user_confirmation_url_action: Option, + pub use_vcn: Option, + pub name: Option, +} + +#[derive(Debug, Serialize)] +pub struct Item { + pub display_name: String, + pub sku: String, + pub unit_price: MinorUnit, + pub qty: i64, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Shipping { + pub name: Name, + pub address: Address, + #[serde(skip_serializing_if = "Option::is_none")] + pub phone_number: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub email: Option, +} +#[derive(Debug, Serialize)] +pub struct Billing { + pub name: Name, + pub address: Address, + #[serde(skip_serializing_if = "Option::is_none")] + pub phone_number: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub email: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Name { + pub first: Option>, + pub last: Option>, + pub full: Option>, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Address { + pub line1: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub line2: Option>, + pub city: Option, + pub state: Option>, + pub zipcode: Option>, + pub country: Option, +} + +#[derive(Debug, Serialize)] +pub struct Metadata { + #[serde(skip_serializing_if = "Option::is_none")] + pub shipping_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub entity_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub platform_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub platform_version: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub platform_affirm: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub webhook_session_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub customer: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub itinerary: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub checkout_channel_type: Option, + #[serde(rename = "BOPIS", skip_serializing_if = "Option::is_none")] + pub bopis: Option, +} + +#[derive(Debug, Serialize)] +pub struct Discount { + pub discount_amount: MinorUnit, + pub discount_display_name: String, + pub discount_code: Option, } impl TryFrom<&AffirmRouterData<&PaymentsAuthorizeRouterData>> for AffirmPaymentsRequest { type Error = error_stack::Report; + fn try_from( item: &AffirmRouterData<&PaymentsAuthorizeRouterData>, ) -> Result { - match item.router_data.request.payment_method_data.clone() { - PaymentMethodData::Card(_) => Err(errors::ConnectorError::NotImplemented( - "Card payment method not implemented".to_string(), - ) - .into()), + let router_data = &item.router_data; + let request = &router_data.request; + + let billing = Some(Billing { + name: Name { + first: item.router_data.get_optional_billing_first_name(), + last: item.router_data.get_optional_billing_last_name(), + full: item.router_data.get_optional_billing_full_name(), + }, + address: Address { + line1: item.router_data.get_optional_billing_line1(), + line2: item.router_data.get_optional_billing_line2(), + city: item.router_data.get_optional_billing_city(), + state: item.router_data.get_optional_billing_state(), + zipcode: item.router_data.get_optional_billing_zip(), + country: item.router_data.get_optional_billing_country(), + }, + phone_number: item.router_data.get_optional_billing_phone_number(), + email: item.router_data.get_optional_billing_email(), + }); + + let shipping = Some(Shipping { + name: Name { + first: item.router_data.get_optional_shipping_first_name(), + last: item.router_data.get_optional_shipping_last_name(), + full: item.router_data.get_optional_shipping_full_name(), + }, + address: Address { + line1: item.router_data.get_optional_shipping_line1(), + line2: item.router_data.get_optional_shipping_line2(), + city: item.router_data.get_optional_shipping_city(), + state: item.router_data.get_optional_shipping_state(), + zipcode: item.router_data.get_optional_shipping_zip(), + country: item.router_data.get_optional_shipping_country(), + }, + phone_number: item.router_data.get_optional_shipping_phone_number(), + email: item.router_data.get_optional_shipping_email(), + }); + + match request.payment_method_data.clone() { + PaymentMethodData::PayLater(PayLaterData::AffirmRedirect {}) => { + let items = match request.order_details.clone() { + Some(order_details) => order_details + .iter() + .map(|data| { + Ok(Item { + display_name: data.product_name.clone(), + sku: data.product_id.clone().unwrap_or_default(), + unit_price: data.amount, + qty: data.quantity.into(), + }) + }) + .collect::, _>>(), + None => Err(report!(errors::ConnectorError::MissingRequiredField { + field_name: "order_details", + })), + }?; + + let auth_type = AffirmAuthType::try_from(&item.router_data.connector_auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let public_api_key = auth_type.public_key; + let merchant = Merchant { + public_api_key, + user_confirmation_url: request.get_complete_authorize_url()?, + user_cancel_url: request.get_router_return_url()?, + user_confirmation_url_action: None, + use_vcn: None, + name: None, + }; + + Ok(Self { + merchant, + items, + shipping, + billing, + total: item.amount, + currency: request.currency, + order_id: Some(item.router_data.connector_request_reference_id.clone()), + }) + } _ => Err(errors::ConnectorError::NotImplemented("Payment method".to_string()).into()), } } } - -//TODO: Fill the struct with respective fields -// Auth Struct pub struct AffirmAuthType { - pub(super) api_key: Secret, + pub public_key: Secret, + pub private_key: Secret, } impl TryFrom<&ConnectorAuthType> for AffirmAuthType { type Error = error_stack::Report; fn try_from(auth_type: &ConnectorAuthType) -> Result { match auth_type { - ConnectorAuthType::HeaderKey { api_key } => Ok(Self { - api_key: api_key.to_owned(), + ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self { + public_key: api_key.to_owned(), + private_key: key1.to_owned(), }), _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), } } } -// PaymentsResponse -//TODO: Append the remaining status flags -#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum AffirmPaymentStatus { - Succeeded, - Failed, - #[default] - Processing, -} -impl From for common_enums::AttemptStatus { - fn from(item: AffirmPaymentStatus) -> Self { +impl From for common_enums::AttemptStatus { + fn from(item: AffirmTransactionStatus) -> Self { match item { - AffirmPaymentStatus::Succeeded => Self::Charged, - AffirmPaymentStatus::Failed => Self::Failure, - AffirmPaymentStatus::Processing => Self::Authorizing, + AffirmTransactionStatus::Authorized => Self::Authorized, + AffirmTransactionStatus::AuthExpired => Self::Failure, + AffirmTransactionStatus::Canceled => Self::Voided, + AffirmTransactionStatus::Captured => Self::Charged, + AffirmTransactionStatus::ConfirmationExpired => Self::Failure, + AffirmTransactionStatus::Confirmed => Self::Authorized, + AffirmTransactionStatus::Created => Self::Pending, + AffirmTransactionStatus::Declined => Self::Failure, + AffirmTransactionStatus::Disputed => Self::Unresolved, + AffirmTransactionStatus::DisputeRefunded => Self::Unresolved, + AffirmTransactionStatus::ExpiredAuthorization => Self::Failure, + AffirmTransactionStatus::ExpiredConfirmation => Self::Failure, + AffirmTransactionStatus::PartiallyCaptured => Self::Charged, + AffirmTransactionStatus::Voided => Self::Voided, + AffirmTransactionStatus::PartiallyVoided => Self::Voided, } } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct AffirmPaymentsResponse { - status: AffirmPaymentStatus, - id: String, +impl From for common_enums::RefundStatus { + fn from(item: AffirmRefundStatus) -> Self { + match item { + AffirmRefundStatus::PartiallyRefunded => Self::Success, + AffirmRefundStatus::Refunded => Self::Success, + } + } } -impl TryFrom> +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct AffirmPaymentsResponse { + checkout_id: String, + redirect_url: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct AffirmCompleteAuthorizeResponse { + pub id: String, + pub status: AffirmTransactionStatus, + pub amount: MinorUnit, + pub amount_refunded: MinorUnit, + pub authorization_expiration: String, + pub checkout_id: String, + pub created: String, + pub currency: Currency, + pub events: Vec, + pub settlement_transaction_id: Option, + pub transaction_id: String, + pub order_id: String, + pub shipping_carrier: Option, + pub shipping_confirmation: Option, + pub shipping: Option, + pub agent_alias: Option, + pub merchant_transaction_id: Option, + pub provider_id: Option, + pub remove_tax: Option, + pub checkout: Option, + pub refund_expires: Option, + pub remaining_capturable_amount: Option, + pub loan_information: Option, + pub user_id: Option, + pub platform: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct TransactionEvent { + pub id: String, + pub amount: MinorUnit, + pub created: String, + pub currency: Currency, + pub fee: Option, + pub fee_refunded: Option, + pub reference_id: Option, + #[serde(rename = "type")] + pub event_type: AffirmEventType, + pub settlement_transaction_id: Option, + pub transaction_id: String, + pub order_id: String, + pub shipping_carrier: Option, + pub shipping_confirmation: Option, + pub shipping: Option, + pub agent_alias: Option, + pub merchant_transaction_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct LoanInformation { + pub fees: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct LoanFees { + pub amount: Option, + pub description: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AffirmTransactionStatus { + Authorized, + AuthExpired, + Canceled, + Captured, + ConfirmationExpired, + Confirmed, + Created, + Declined, + Disputed, + DisputeRefunded, + ExpiredAuthorization, + ExpiredConfirmation, + PartiallyCaptured, + Voided, + PartiallyVoided, +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AffirmRefundStatus { + PartiallyRefunded, + Refunded, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct AffirmPSyncResponse { + pub amount: MinorUnit, + pub amount_refunded: MinorUnit, + pub authorization_expiration: Option, + pub checkout_id: String, + pub created: String, + pub currency: Currency, + pub events: Vec, + pub id: String, + pub order_id: String, + pub provider_id: Option, + pub remove_tax: Option, + pub status: AffirmTransactionStatus, + pub checkout: Option, + pub refund_expires: Option, + pub remaining_capturable_amount: Option, + pub loan_information: Option, + pub shipping_carrier: Option, + pub shipping_confirmation: Option, + pub shipping: Option, + pub merchant_transaction_id: Option, + pub settlement_transaction_id: Option, + pub transaction_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct AffirmRsyncResponse { + pub amount: MinorUnit, + pub amount_refunded: MinorUnit, + pub authorization_expiration: String, + pub checkout_id: String, + pub created: String, + pub currency: Currency, + pub events: Vec, + pub id: String, + pub order_id: String, + pub status: AffirmRefundStatus, + pub refund_expires: Option, + pub remaining_capturable_amount: Option, + pub shipping_carrier: Option, + pub shipping_confirmation: Option, + pub shipping: Option, + pub merchant_transaction_id: Option, + pub settlement_transaction_id: Option, + pub transaction_id: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum AffirmResponseWrapper { + Authorize(AffirmPaymentsResponse), + Psync(Box), +} + +impl TryFrom> + for RouterData +{ + type Error = error_stack::Report; + + fn try_from( + item: ResponseRouterData, + ) -> Result { + match &item.response { + AffirmResponseWrapper::Authorize(resp) => { + let redirection_data = url::Url::parse(&resp.redirect_url) + .ok() + .map(|url| RedirectForm::from((url, Method::Get))); + + Ok(Self { + status: enums::AttemptStatus::AuthenticationPending, + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(resp.checkout_id.clone()), + redirection_data: Box::new(redirection_data), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + charges: None, + incremental_authorization_allowed: None, + }), + ..item.data + }) + } + AffirmResponseWrapper::Psync(resp) => { + let status = enums::AttemptStatus::from(resp.status); + Ok(Self { + status, + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(resp.id.clone()), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + charges: None, + incremental_authorization_allowed: None, + }), + ..item.data + }) + } + } + } +} + +impl TryFrom> for RouterData { type Error = error_stack::Report; fn try_from( - item: ResponseRouterData, + item: ResponseRouterData, ) -> Result { Ok(Self { status: common_enums::AttemptStatus::from(item.response.status), @@ -129,26 +527,26 @@ impl TryFrom, } impl TryFrom<&AffirmRouterData<&RefundsRouterData>> for AffirmRefundRequest { type Error = error_stack::Report; + fn try_from(item: &AffirmRouterData<&RefundsRouterData>) -> Result { + let reference_id = item.router_data.request.connector_transaction_id.clone(); + Ok(Self { amount: item.amount.to_owned(), + reference_id: Some(reference_id), }) } } -// Type definition for Refund Response - #[allow(dead_code)] #[derive(Debug, Copy, Serialize, Default, Deserialize, Clone)] pub enum RefundStatus { @@ -158,28 +556,58 @@ pub enum RefundStatus { Processing, } -impl From for enums::RefundStatus { - fn from(item: RefundStatus) -> Self { - match item { - RefundStatus::Succeeded => Self::Success, - RefundStatus::Failed => Self::Failure, - RefundStatus::Processing => Self::Pending, - //TODO: Review mapping +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct AffirmRefundResponse { + pub id: String, + pub amount: MinorUnit, + pub created: String, + pub currency: Currency, + pub fee: Option, + pub fee_refunded: Option, + pub reference_id: Option, + #[serde(rename = "type")] + pub event_type: AffirmEventType, + pub settlement_transaction_id: Option, + pub transaction_id: String, + pub order_id: String, + pub shipping_carrier: Option, + pub shipping_confirmation: Option, + pub shipping: Option, + pub agent_alias: Option, + pub merchant_transaction_id: Option, +} + +impl From for enums::RefundStatus { + fn from(event_type: AffirmEventType) -> Self { + match event_type { + AffirmEventType::Refund => Self::Success, + _ => Self::Pending, } } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct RefundResponse { - id: String, - status: RefundStatus, -} - -impl TryFrom> for RefundsRouterData { +impl TryFrom> + for RefundsRouterData +{ type Error = error_stack::Report; fn try_from( - item: RefundsResponseRouterData, + item: RefundsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(RefundsResponseData { + connector_refund_id: item.response.id.to_string(), + refund_status: enums::RefundStatus::from(item.response.event_type), + }), + ..item.data + }) + } +} + +impl TryFrom> for RefundsRouterData { + type Error = error_stack::Report; + fn try_from( + item: RefundsResponseRouterData, ) -> Result { Ok(Self { response: Ok(RefundsResponseData { @@ -191,29 +619,205 @@ impl TryFrom> for RefundsRout } } -impl TryFrom> for RefundsRouterData { - type Error = error_stack::Report; - fn try_from( - item: RefundsResponseRouterData, - ) -> Result { - Ok(Self { - response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), - }), - ..item.data - }) - } -} - -//TODO: Fill the struct with respective fields #[derive(Default, Debug, Serialize, Deserialize, PartialEq)] pub struct AffirmErrorResponse { pub status_code: u16, pub code: String, pub message: String, - pub reason: Option, - pub network_advice_code: Option, - pub network_decline_code: Option, - pub network_error_message: Option, + #[serde(rename = "type")] + pub error_type: String, +} + +#[derive(Debug, Serialize)] +pub struct AffirmCaptureRequest { + pub order_id: Option, + pub reference_id: Option, + pub amount: MinorUnit, + pub shipping_carrier: Option, + pub shipping_confirmation: Option, +} + +impl TryFrom<&AffirmRouterData<&PaymentsCaptureRouterData>> for AffirmCaptureRequest { + type Error = error_stack::Report; + + fn try_from(item: &AffirmRouterData<&PaymentsCaptureRouterData>) -> Result { + let reference_id = match item.router_data.connector_request_reference_id.clone() { + ref_id if ref_id.is_empty() => None, + ref_id => Some(ref_id), + }; + + let amount = item.amount; + + Ok(Self { + reference_id, + amount, + order_id: None, + shipping_carrier: None, + shipping_confirmation: None, + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct AffirmCaptureResponse { + pub id: String, + pub amount: MinorUnit, + pub created: String, + pub currency: Currency, + pub fee: Option, + pub fee_refunded: Option, + pub reference_id: Option, + #[serde(rename = "type")] + pub event_type: AffirmEventType, + pub settlement_transaction_id: Option, + pub transaction_id: String, + pub order_id: String, + pub shipping_carrier: Option, + pub shipping_confirmation: Option, + pub shipping: Option, + pub agent_alias: Option, + pub merchant_transaction_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum AffirmEventType { + Auth, + AuthExpired, + Capture, + ChargeOff, + Confirm, + ConfirmationExpired, + ExpireAuthorization, + ExpireConfirmation, + Refund, + SplitCapture, + Update, + Void, + PartialVoid, + RefundVoided, +} + +impl From for enums::AttemptStatus { + fn from(event_type: AffirmEventType) -> Self { + match event_type { + AffirmEventType::Auth => Self::Authorized, + AffirmEventType::Capture | AffirmEventType::SplitCapture | AffirmEventType::Confirm => { + Self::Charged + } + AffirmEventType::AuthExpired + | AffirmEventType::ChargeOff + | AffirmEventType::ConfirmationExpired + | AffirmEventType::ExpireAuthorization + | AffirmEventType::ExpireConfirmation => Self::Failure, + AffirmEventType::Refund | AffirmEventType::RefundVoided => Self::AutoRefunded, + AffirmEventType::Update => Self::Pending, + AffirmEventType::Void | AffirmEventType::PartialVoid => Self::Voided, + } + } +} + +impl + TryFrom> + for RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData< + F, + AffirmCaptureResponse, + PaymentsCaptureData, + PaymentsResponseData, + >, + ) -> Result { + Ok(Self { + status: enums::AttemptStatus::from(item.response.event_type.clone()), + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.id), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charges: None, + }), + ..item.data + }) + } +} + +impl TryFrom<&PaymentsCancelRouterData> for AffirmCancelRequest { + type Error = error_stack::Report; + fn try_from(item: &PaymentsCancelRouterData) -> Result { + let request = &item.request; + + let reference_id = request.connector_transaction_id.clone(); + let amount = item + .request + .amount + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "amount", + })?; + Ok(Self { + reference_id: Some(reference_id), + amount, + merchant_transaction_id: None, + }) + } +} + +#[derive(Debug, Serialize)] +pub struct AffirmCancelRequest { + pub reference_id: Option, + pub amount: i64, + pub merchant_transaction_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct AffirmCancelResponse { + pub id: String, + pub amount: MinorUnit, + pub created: String, + pub currency: Currency, + pub fee: Option, + pub fee_refunded: Option, + pub reference_id: Option, + #[serde(rename = "type")] + pub event_type: AffirmEventType, + pub settlement_transaction_id: Option, + pub transaction_id: String, + pub order_id: String, + pub shipping_carrier: Option, + pub shipping_confirmation: Option, + pub shipping: Option, + pub agent_alias: Option, + pub merchant_transaction_id: Option, +} + +impl + TryFrom> + for RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + Ok(Self { + status: enums::AttemptStatus::from(item.response.event_type.clone()), + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.id), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charges: None, + }), + ..item.data + }) + } } diff --git a/crates/hyperswitch_connectors/src/default_implementations.rs b/crates/hyperswitch_connectors/src/default_implementations.rs index 64c30a937f..6b8b494882 100644 --- a/crates/hyperswitch_connectors/src/default_implementations.rs +++ b/crates/hyperswitch_connectors/src/default_implementations.rs @@ -1143,7 +1143,6 @@ default_imp_for_complete_authorize!( connectors::Aci, connectors::Adyen, connectors::Adyenplatform, - connectors::Affirm, connectors::Amazonpay, connectors::Archipel, connectors::Authipay, diff --git a/crates/router/src/core/connector_validation.rs b/crates/router/src/core/connector_validation.rs index a0665a4f9e..d5c9fbc927 100644 --- a/crates/router/src/core/connector_validation.rs +++ b/crates/router/src/core/connector_validation.rs @@ -80,6 +80,10 @@ impl ConnectorAuthTypeAndMetadataValidation<'_> { )?; Ok(()) } + api_enums::Connector::Affirm => { + affirm::transformers::AffirmAuthType::try_from(self.auth_type)?; + Ok(()) + } api_enums::Connector::Airwallex => { airwallex::transformers::AirwallexAuthType::try_from(self.auth_type)?; Ok(()) diff --git a/crates/router/src/types/api/connector_mapping.rs b/crates/router/src/types/api/connector_mapping.rs index 1886804229..1298296f30 100644 --- a/crates/router/src/types/api/connector_mapping.rs +++ b/crates/router/src/types/api/connector_mapping.rs @@ -108,6 +108,9 @@ impl ConnectorData { enums::Connector::Adyen => { Ok(ConnectorEnum::Old(Box::new(connector::Adyen::new()))) } + enums::Connector::Affirm => { + Ok(ConnectorEnum::Old(Box::new(connector::Affirm::new()))) + } enums::Connector::Adyenplatform => Ok(ConnectorEnum::Old(Box::new( connector::Adyenplatform::new(), ))), diff --git a/crates/router/src/types/api/feature_matrix.rs b/crates/router/src/types/api/feature_matrix.rs index 8d5ff7d4f1..95150d98c2 100644 --- a/crates/router/src/types/api/feature_matrix.rs +++ b/crates/router/src/types/api/feature_matrix.rs @@ -22,6 +22,9 @@ impl FeatureMatrixConnectorData { enums::Connector::Adyen => { Ok(ConnectorEnum::Old(Box::new(connector::Adyen::new()))) } + enums::Connector::Affirm => { + Ok(ConnectorEnum::Old(Box::new(connector::Affirm::new()))) + } enums::Connector::Adyenplatform => Ok(ConnectorEnum::Old(Box::new( connector::Adyenplatform::new(), ))), diff --git a/crates/router/src/types/connector_transformers.rs b/crates/router/src/types/connector_transformers.rs index 9d7c568cf3..05321d29c2 100644 --- a/crates/router/src/types/connector_transformers.rs +++ b/crates/router/src/types/connector_transformers.rs @@ -9,6 +9,7 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { Ok(match from { api_enums::Connector::Aci => Self::Aci, api_enums::Connector::Adyen => Self::Adyen, + api_enums::Connector::Affirm => Self::Affirm, api_enums::Connector::Adyenplatform => Self::Adyenplatform, api_enums::Connector::Airwallex => Self::Airwallex, // api_enums::Connector::Amazonpay => Self::Amazonpay, diff --git a/cypress-tests/cypress/e2e/configs/mock-server/Connectors/Affirm.ts b/cypress-tests/cypress/e2e/configs/mock-server/Connectors/Affirm.ts new file mode 100644 index 0000000000..7d01d634d1 --- /dev/null +++ b/cypress-tests/cypress/e2e/configs/mock-server/Connectors/Affirm.ts @@ -0,0 +1,359 @@ +/* eslint-disable no-console */ +import * as express from "express"; +import type { NextFunction, Request, Response, Router } from "express"; +import cors from "cors"; +import { Buffer } from "buffer"; + +const router: Router = express.default.Router(); +const PORT = process.env.PORT || 3011; // Using a different port for Affirm + +// Middleware +router.use(cors()); +router.use(express.default.json()); +router.use(express.default.urlencoded({ extended: true })); + +// Mock data storage +interface MockData { + transactions: Record; + refunds: Record; + checkouts: Record; +} + +const mockData: MockData = { + transactions: {}, + refunds: {}, + checkouts: {}, +}; + +// Valid API keys (public_key:private_key) +const validApiKeys: Record = { + public_key_123: "private_key_456", +}; + +// Helper functions +function generateId(prefix: string): string { + return `${prefix}_${Math.random().toString(36).substr(2, 16)}`; +} + +function getCurrentTimestamp(): string { + return new Date().toISOString(); +} + +// Authentication middleware +function authenticateApiKey( + req: Request, + res: Response, + next: NextFunction +): void { + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.startsWith("Basic ")) { + res.status(401).json({ + status_code: 401, + code: "unauthorized", + message: "Missing or invalid Authorization header", + error_type: "authentication_error", + }); + return; + } + + const token = authHeader.split(" ")[1]; + const decodedToken = Buffer.from(token, "base64").toString("utf8"); + const [publicKey, privateKey] = decodedToken.split(":"); + + if (!publicKey || !privateKey || validApiKeys[publicKey] !== privateKey) { + res.status(401).json({ + status_code: 401, + code: "unauthorized", + message: "Invalid API key", + error_type: "authentication_error", + }); + return; + } + + next(); +} + +// Logging middleware +router.use((req: Request, res: Response, next: NextFunction): void => { + console.log( + `[Affirm Mock] ${new Date().toISOString()} - ${req.method} ${req.path}` + ); + if (Object.keys(req.headers).length > 0) + console.log("Headers:", JSON.stringify(req.headers, null, 2)); + if (Object.keys(req.body).length > 0) + console.log("Body:", JSON.stringify(req.body, null, 2)); + next(); +}); + +// Health check endpoint +router.get("/health", (req: Request, res: Response): void => { + res.json({ + status: "success", + msg: "Affirm Mock Server is running", + timestamp: getCurrentTimestamp(), + }); +}); + +// 1. POST /v2/checkout/direct - Create a checkout +router.post( + "/v2/checkout/direct", + authenticateApiKey, + (req: Request, res: Response) => { + const { merchant, total } = req.body; + + if (!merchant || !total) { + return res.status(400).json({ + status_code: 400, + code: "bad_request", + message: "Missing required fields", + error_type: "invalid_request_error", + }); + } + + const checkoutId = generateId("checkout"); + const redirectUrl = `${merchant.user_confirmation_url}?checkout_token=${checkoutId}`; + + const checkout = { + checkout_id: checkoutId, + redirect_url: redirectUrl, + ...req.body, + }; + mockData.checkouts[checkoutId] = checkout; + + res.status(200).json({ + checkout_id: checkoutId, + redirect_url: redirectUrl, + }); + } +); + +// 2. POST /v1/transactions - Complete authorize (read charge) +router.post( + "/v1/transactions", + authenticateApiKey, + (req: Request, res: Response) => { + const { transaction_id } = req.body; + + if (!transaction_id) { + return res.status(400).json({ + status_code: 400, + code: "bad_request", + message: "Missing transaction_id", + error_type: "invalid_request_error", + }); + } + + const checkout = mockData.checkouts[transaction_id]; + if (!checkout) { + return res.status(404).json({ + status_code: 404, + code: "not_found", + message: "Checkout not found", + error_type: "invalid_request_error", + }); + } + + const transactionId = generateId("txn"); + const now = getCurrentTimestamp(); + const transaction = { + id: transactionId, + status: "authorized", + amount: checkout.total, + currency: checkout.currency, + created: now, + order_id: checkout.order_id, + checkout_id: transaction_id, + events: [ + { + id: generateId("evt"), + type: "auth", + created: now, + }, + ], + }; + mockData.transactions[transactionId] = transaction; + + res.status(200).json(transaction); + } +); + +// 3. GET /v1/transactions/:transactionId - PSync +router.get( + "/v1/transactions/:transactionId", + authenticateApiKey, + (req: Request, res: Response) => { + const { transactionId } = req.params; + const transaction = mockData.transactions[transactionId]; + + if (!transaction) { + return res.status(404).json({ + status_code: 404, + code: "not_found", + message: "Transaction not found", + error_type: "invalid_request_error", + }); + } + + res.status(200).json(transaction); + } +); + +// 4. POST /v1/transactions/:transactionId/capture - Capture +router.post( + "/v1/transactions/:transactionId/capture", + authenticateApiKey, + (req: Request, res: Response) => { + const { transactionId } = req.params; + const transaction = mockData.transactions[transactionId]; + + if (!transaction) { + return res.status(404).json({ + status_code: 404, + code: "not_found", + message: "Transaction not found", + error_type: "invalid_request_error", + }); + } + + if (transaction.status !== "authorized") { + return res.status(400).json({ + status_code: 400, + code: "bad_request", + message: "Transaction is not authorized", + error_type: "invalid_request_error", + }); + } + + transaction.status = "captured"; + const event = { + id: generateId("evt"), + type: "capture", + created: getCurrentTimestamp(), + }; + transaction.events.push(event); + + res.status(200).json(event); + } +); + +// 5. POST /v1/transactions/:transactionId/void - Void +router.post( + "/v1/transactions/:transactionId/void", + authenticateApiKey, + (req: Request, res: Response) => { + const { transactionId } = req.params; + const transaction = mockData.transactions[transactionId]; + + if (!transaction) { + return res.status(404).json({ + status_code: 404, + code: "not_found", + message: "Transaction not found", + error_type: "invalid_request_error", + }); + } + + if (transaction.status === "captured") { + return res.status(400).json({ + status_code: 400, + code: "bad_request", + message: "Cannot void a captured transaction", + error_type: "invalid_request_error", + }); + } + + transaction.status = "voided"; + const event = { + id: generateId("evt"), + type: "void", + created: getCurrentTimestamp(), + }; + transaction.events.push(event); + + res.status(200).json(event); + } +); + +// 6. POST /v1/transactions/:transactionId/refund - Refund +router.post( + "/v1/transactions/:transactionId/refund", + authenticateApiKey, + (req: Request, res: Response) => { + const { transactionId } = req.params; + const { amount } = req.body; + const transaction = mockData.transactions[transactionId]; + + if (!transaction) { + return res.status(404).json({ + status_code: 404, + code: "not_found", + message: "Transaction not found", + error_type: "invalid_request_error", + }); + } + + if (transaction.status !== "captured") { + return res.status(400).json({ + status_code: 400, + code: "bad_request", + message: "Cannot refund a non-captured transaction", + error_type: "invalid_request_error", + }); + } + + if (amount > transaction.amount) { + return res.status(400).json({ + status_code: 400, + code: "bad_request", + message: "Refund amount exceeds transaction amount", + error_type: "invalid_request_error", + }); + } + + const refundId = generateId("ref"); + const now = getCurrentTimestamp(); + const refund = { + id: refundId, + amount, + created: now, + currency: transaction.currency, + transaction_id: transactionId, + type: "refund", + }; + mockData.refunds[refundId] = refund; + + transaction.status = "refunded"; + transaction.events.push(refund); + + res.status(200).json(refund); + } +); + +// Error handling middleware +router.use( + (err: Error, req: Request, res: Response, _next: NextFunction): void => { + console.error("[Affirm Mock] Error:", err); + res.status(500).json({ + status_code: 500, + code: "internal_server_error", + message: "An unexpected error occurred", + error_type: "api_error", + }); + } +); + +// 404 handler +router.use((req: Request, res: Response): void => { + res.status(404).json({ + status_code: 404, + code: "not_found", + message: `Endpoint ${req.method} ${req.originalUrl} not found`, + error_type: "invalid_request_error", + }); +}); + +console.log(`🚀 Affirm Mock Server running on port ${PORT}`); +console.log(`📍 Server URL: http://localhost:${PORT}`); + +export default router; diff --git a/cypress-tests/cypress/e2e/configs/mock-server/Creds.json b/cypress-tests/cypress/e2e/configs/mock-server/Creds.json index dbe1719fd2..87c20fd9e4 100644 --- a/cypress-tests/cypress/e2e/configs/mock-server/Creds.json +++ b/cypress-tests/cypress/e2e/configs/mock-server/Creds.json @@ -12,5 +12,12 @@ "key1": "testsecret456", "api_secret": "testsecret456" } + }, + "affirm": { + "connector_account_details": { + "auth_type": "BodyKey", + "api_key": "affirm-testkey123", + "key1": "affirm-testsecret456" + } } } diff --git a/cypress-tests/cypress/e2e/configs/mock-server/router.ts b/cypress-tests/cypress/e2e/configs/mock-server/router.ts index f1491773a6..963acfda01 100644 --- a/cypress-tests/cypress/e2e/configs/mock-server/router.ts +++ b/cypress-tests/cypress/e2e/configs/mock-server/router.ts @@ -10,6 +10,7 @@ import type { // Import from TypeScript version import silverflowApp from "./Connectors/Silverflow.ts"; import celeroApp from "./Connectors/Celero.ts"; +import affirmApp from "./Connectors/Affirm.ts"; // TODO: Update to import from TypeScript version once fully tested // import silverflowApp from "./Silverflow"; @@ -21,6 +22,7 @@ interface MockRouters { const mockRouters: MockRouters = { silverflow: silverflowApp, celero: celeroApp, + affirm: affirmApp, }; // Create a router diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 9b81a63e18..b49713eb0c 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -425,6 +425,9 @@ paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,GB,SE,NO,SK,AT,N klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NO,PL,PT,RO,ES,SE,CH,NL,GB,US", currency = "AUD,EUR,CAD,CZK,DKK,NOK,PLN,RON,SEK,CHF,GBP,USD" } ideal = { country = "NL", currency = "EUR" } +[pm_filters.affirm] +affirm = { country = "CA,US", currency = "CAD,USD" } + [pm_filters.bambora] credit = { country = "US,CA", currency = "USD" } debit = { country = "US,CA", currency = "USD" }