Files
2023-03-10 09:19:58 +00:00

677 lines
27 KiB
Rust

mod client;
pub(crate) mod request;
use std::{
collections::HashMap,
fmt::Debug,
future::Future,
str,
time::{Duration, Instant},
};
use actix_web::{body, http::header, HttpRequest, HttpResponse, Responder};
use common_utils::errors::ReportSwitchExt;
use error_stack::{report, IntoReport, Report, ResultExt};
use masking::ExposeOptionInterface;
use router_env::{instrument, tracing, Tag};
use serde::Serialize;
use self::request::{ContentType, HeaderExt, RequestBuilderExt};
pub use self::request::{Method, Request, RequestBuilder};
use crate::{
configs::settings::Connectors,
consts,
core::{
errors::{self, CustomResult, RouterResult},
payments,
},
db::StorageInterface,
logger,
routes::{app::AppStateInfo, AppState},
services::authentication as auth,
types::{self, api, storage, ErrorResponse},
};
pub type BoxedConnectorIntegration<'a, T, Req, Resp> =
Box<&'a (dyn ConnectorIntegration<T, Req, Resp> + Send + Sync)>;
pub trait ConnectorIntegrationAny<T, Req, Resp>: Send + Sync + 'static {
fn get_connector_integration(&self) -> BoxedConnectorIntegration<'_, T, Req, Resp>;
}
impl<S, T, Req, Resp> ConnectorIntegrationAny<T, Req, Resp> for S
where
S: ConnectorIntegration<T, Req, Resp> + Send + Sync,
{
fn get_connector_integration(&self) -> BoxedConnectorIntegration<'_, T, Req, Resp> {
Box::new(self)
}
}
#[async_trait::async_trait]
pub trait ConnectorIntegration<T, Req, Resp>: ConnectorIntegrationAny<T, Req, Resp> + Sync {
fn get_headers(
&self,
_req: &types::RouterData<T, Req, Resp>,
_connectors: &Connectors,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
Ok(vec![])
}
fn get_content_type(&self) -> &'static str {
mime::APPLICATION_JSON.essence_str()
}
/// primarily used when creating signature based on request method of payment flow
fn get_http_method(&self) -> Method {
Method::Post
}
fn get_url(
&self,
_req: &types::RouterData<T, Req, Resp>,
_connectors: &Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(String::new())
}
fn get_request_body(
&self,
_req: &types::RouterData<T, Req, Resp>,
) -> CustomResult<Option<String>, errors::ConnectorError> {
Ok(None)
}
/// This module can be called before executing a payment flow where a pre-task is needed
/// Eg: Some connectors requires one-time session token before making a payment, we can add the session token creation logic in this block
async fn execute_pretasks(
&self,
_router_data: &mut types::RouterData<T, Req, Resp>,
_app_state: &AppState,
) -> CustomResult<(), errors::ConnectorError> {
Ok(())
}
/// This module can be called after executing a payment flow where a post-task needed
/// Eg: Some connectors require payment sync to happen immediately after the authorize call to complete the transaction, we can add that logic in this block
async fn execute_posttasks(
&self,
_router_data: &mut types::RouterData<T, Req, Resp>,
_app_state: &AppState,
) -> CustomResult<(), errors::ConnectorError> {
Ok(())
}
fn build_request(
&self,
_req: &types::RouterData<T, Req, Resp>,
_connectors: &Connectors,
) -> CustomResult<Option<Request>, errors::ConnectorError> {
Ok(None)
}
fn handle_response(
&self,
data: &types::RouterData<T, Req, Resp>,
_res: types::Response,
) -> CustomResult<types::RouterData<T, Req, Resp>, errors::ConnectorError>
where
T: Clone,
Req: Clone,
Resp: Clone,
{
Ok(data.clone())
}
fn get_error_response(
&self,
_res: types::Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
Ok(ErrorResponse::get_not_implemented())
}
fn get_certificate(
&self,
_req: &types::RouterData<T, Req, Resp>,
) -> CustomResult<Option<String>, errors::ConnectorError> {
Ok(None)
}
fn get_certificate_key(
&self,
_req: &types::RouterData<T, Req, Resp>,
) -> CustomResult<Option<String>, errors::ConnectorError> {
Ok(None)
}
}
#[instrument(skip_all)]
pub async fn execute_connector_processing_step<
'b,
'a,
T: 'static,
Req: Debug + Clone + 'static,
Resp: Debug + Clone + 'static,
>(
state: &'b AppState,
connector_integration: BoxedConnectorIntegration<'a, T, Req, Resp>,
req: &'b types::RouterData<T, Req, Resp>,
call_connector_action: payments::CallConnectorAction,
) -> CustomResult<types::RouterData<T, Req, Resp>, errors::ConnectorError>
where
T: Clone + Debug,
// BoxedConnectorIntegration<T, Req, Resp>: 'b,
{
// If needed add an error stack as follows
// connector_integration.build_request(req).attach_printable("Failed to build request");
let mut router_data = req.clone();
match call_connector_action {
payments::CallConnectorAction::HandleResponse(res) => {
let response = types::Response {
response: res.into(),
status_code: 200,
};
connector_integration.handle_response(req, response)
}
payments::CallConnectorAction::Avoid => Ok(router_data),
payments::CallConnectorAction::StatusUpdate(status) => {
router_data.status = status;
Ok(router_data)
}
payments::CallConnectorAction::Trigger => {
match connector_integration.build_request(req, &state.conf.connectors)? {
Some(request) => {
logger::debug!(connector_request=?request);
let response = call_connector_api(state, request).await;
logger::debug!(connector_response=?response);
match response {
Ok(body) => {
let response = match body {
Ok(body) => connector_integration.handle_response(req, body)?,
Err(body) => {
let error = connector_integration.get_error_response(body)?;
router_data.response = Err(error);
router_data
}
};
Ok(response)
}
Err(error) => Err(error
.change_context(errors::ConnectorError::ProcessingStepFailed(None))),
}
}
None => Ok(router_data),
}
}
}
}
#[instrument(skip_all)]
pub async fn call_connector_api(
state: &AppState,
request: Request,
) -> CustomResult<Result<types::Response, types::Response>, errors::ApiClientError> {
let current_time = Instant::now();
let response = send_request(state, request).await;
let elapsed_time = current_time.elapsed();
logger::info!(request_time=?elapsed_time);
handle_response(response).await
}
#[instrument(skip_all)]
async fn send_request(
state: &AppState,
request: Request,
) -> CustomResult<reqwest::Response, errors::ApiClientError> {
logger::debug!(method=?request.method, headers=?request.headers, payload=?request.payload, ?request);
let url = &request.url;
let should_bypass_proxy = client::proxy_bypass_urls(&state.conf.locker).contains(url);
let client = client::create_client(
&state.conf.proxy,
should_bypass_proxy,
request.certificate,
request.certificate_key,
)?;
let headers = request.headers.construct_header_map()?;
match request.method {
Method::Get => client.get(url),
Method::Post => {
let client = client.post(url);
match request.content_type {
Some(ContentType::Json) => client.json(&request.payload),
// Currently this is not used remove this if not required
// If using this then handle the serde_part
Some(ContentType::FormUrlEncoded) => {
let url_encoded_payload = serde_urlencoded::to_string(&request.payload)
.into_report()
.change_context(errors::ApiClientError::UrlEncodingFailed)
.attach_printable_lazy(|| {
format!(
"Unable to do url encoding on request: {:?}",
&request.payload
)
})?;
logger::debug!(?url_encoded_payload);
client.body(url_encoded_payload)
}
// If payload needs processing the body cannot have default
None => client.body(request.payload.expose_option().unwrap_or_default()),
}
}
Method::Put => {
client
.put(url)
.body(request.payload.expose_option().unwrap_or_default()) // If payload needs processing the body cannot have default
}
Method::Delete => client.delete(url),
}
.add_headers(headers)
.timeout(Duration::from_secs(crate::consts::REQUEST_TIME_OUT))
.send()
.await
.map_err(|error| match error {
error if error.is_timeout() => errors::ApiClientError::RequestTimeoutReceived,
_ => errors::ApiClientError::RequestNotSent(error.to_string()),
})
.into_report()
.attach_printable("Unable to send request to connector")
}
#[instrument(skip_all)]
async fn handle_response(
response: CustomResult<reqwest::Response, errors::ApiClientError>,
) -> CustomResult<Result<types::Response, types::Response>, errors::ApiClientError> {
response
.map(|response| async {
logger::info!(?response);
let status_code = response.status().as_u16();
match status_code {
200..=202 | 302 => {
logger::debug!(response=?response);
// If needed add log line
// logger:: error!( error_parsing_response=?err);
let response = response
.bytes()
.await
.into_report()
.change_context(errors::ApiClientError::ResponseDecodingFailed)
.attach_printable("Error while waiting for response")?;
Ok(Ok(types::Response {
response,
status_code,
}))
}
status_code @ 500..=599 => {
let bytes = response.bytes().await.map_err(|error| {
report!(error)
.change_context(errors::ApiClientError::ResponseDecodingFailed)
.attach_printable("Client error response received")
})?;
// let error = match status_code {
// 500 => errors::ApiClientError::InternalServerErrorReceived,
// 502 => errors::ApiClientError::BadGatewayReceived,
// 503 => errors::ApiClientError::ServiceUnavailableReceived,
// 504 => errors::ApiClientError::GatewayTimeoutReceived,
// _ => errors::ApiClientError::UnexpectedServerResponse,
// };
Ok(Err(types::Response {
response: bytes,
status_code,
}))
}
status_code @ 400..=499 => {
let bytes = response.bytes().await.map_err(|error| {
report!(error)
.change_context(errors::ApiClientError::ResponseDecodingFailed)
.attach_printable("Client error response received")
})?;
/* let error = match status_code {
400 => errors::ApiClientError::BadRequestReceived(bytes),
401 => errors::ApiClientError::UnauthorizedReceived(bytes),
403 => errors::ApiClientError::ForbiddenReceived,
404 => errors::ApiClientError::NotFoundReceived(bytes),
405 => errors::ApiClientError::MethodNotAllowedReceived,
408 => errors::ApiClientError::RequestTimeoutReceived,
422 => errors::ApiClientError::UnprocessableEntityReceived(bytes),
429 => errors::ApiClientError::TooManyRequestsReceived,
_ => errors::ApiClientError::UnexpectedServerResponse,
};
Err(report!(error).attach_printable("Client error response received"))
*/
Ok(Err(types::Response {
response: bytes,
status_code,
}))
}
_ => Err(report!(errors::ApiClientError::UnexpectedServerResponse)
.attach_printable("Unexpected response from server")),
}
})?
.await
}
#[derive(Debug, Eq, PartialEq)]
pub enum ApplicationResponse<R> {
Json(R),
StatusOk,
TextPlain(String),
JsonForRedirection(api::RedirectionResponse),
Form(RedirectForm),
}
#[derive(Debug, Eq, PartialEq, Serialize)]
pub struct ApplicationRedirectResponse {
pub url: String,
}
impl From<&storage::PaymentAttempt> for ApplicationRedirectResponse {
fn from(payment_attempt: &storage::PaymentAttempt) -> Self {
Self {
url: format!(
"/payments/start/{}/{}/{}",
&payment_attempt.payment_id,
&payment_attempt.merchant_id,
&payment_attempt.attempt_id
),
}
}
}
#[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
pub struct RedirectForm {
pub endpoint: String,
pub method: Method,
pub form_fields: HashMap<String, String>,
}
impl From<(url::Url, Method)> for RedirectForm {
fn from((mut redirect_url, method): (url::Url, Method)) -> Self {
let form_fields = std::collections::HashMap::from_iter(
redirect_url
.query_pairs()
.map(|(key, value)| (key.to_string(), value.to_string())),
);
// Do not include query params in the endpoint
redirect_url.set_query(None);
Self {
endpoint: redirect_url.to_string(),
method,
form_fields,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum AuthFlow {
Client,
Merchant,
}
#[instrument(skip(request, payload, state, func, api_auth))]
pub async fn server_wrap_util<'a, 'b, A, U, T, Q, F, Fut, E, OErr>(
state: &'b A,
request: &'a HttpRequest,
payload: T,
func: F,
api_auth: &dyn auth::AuthenticateAndFetch<U, A>,
) -> CustomResult<ApplicationResponse<Q>, OErr>
where
F: Fn(&'b A, U, T) -> Fut,
Fut: Future<Output = CustomResult<ApplicationResponse<Q>, E>>,
Q: Serialize + Debug + 'a,
T: Debug,
A: AppStateInfo,
CustomResult<ApplicationResponse<Q>, E>: ReportSwitchExt<ApplicationResponse<Q>, OErr>,
CustomResult<U, errors::ApiErrorResponse>: ReportSwitchExt<U, OErr>,
{
let auth_out = api_auth
.authenticate_and_fetch(request.headers(), state)
.await
.switch()?;
func(state, auth_out, payload).await.switch()
}
#[instrument(
skip(request, payload, state, func, api_auth),
fields(request_method, request_url_path)
)]
pub async fn server_wrap<'a, 'b, A, T, U, Q, F, Fut, E>(
state: &'b A,
request: &'a HttpRequest,
payload: T,
func: F,
api_auth: &dyn auth::AuthenticateAndFetch<U, A>,
) -> HttpResponse
where
F: Fn(&'b A, U, T) -> Fut,
Fut: Future<Output = CustomResult<ApplicationResponse<Q>, E>>,
Q: Serialize + Debug + 'a,
T: Debug,
A: AppStateInfo,
CustomResult<ApplicationResponse<Q>, E>:
ReportSwitchExt<ApplicationResponse<Q>, api_models::errors::types::ApiErrorResponse>,
{
let request_method = request.method().as_str();
let url_path = request.path();
tracing::Span::current().record("request_method", request_method);
tracing::Span::current().record("request_url_path", url_path);
let start_instant = Instant::now();
logger::info!(tag = ?Tag::BeginRequest);
let res = match server_wrap_util(state, request, payload, func, api_auth).await {
Ok(ApplicationResponse::Json(response)) => match serde_json::to_string(&response) {
Ok(res) => http_response_json(res),
Err(_) => http_response_err(
r#"{
"error": {
"message": "Error serializing response from connector"
}
}"#,
),
},
Ok(ApplicationResponse::StatusOk) => http_response_ok(),
Ok(ApplicationResponse::TextPlain(text)) => http_response_plaintext(text),
Ok(ApplicationResponse::JsonForRedirection(response)) => {
match serde_json::to_string(&response) {
Ok(res) => http_redirect_response(res, response),
Err(_) => http_response_err(
r#"{
"error": {
"message": "Error serializing response from connector"
}
}"#,
),
}
}
Ok(ApplicationResponse::Form(response)) => build_redirection_form(&response)
.respond_to(request)
.map_into_boxed_body(),
Err(error) => log_and_return_error_response(error),
};
let response_code = res.status().as_u16();
let end_instant = Instant::now();
let request_duration = end_instant.saturating_duration_since(start_instant);
logger::info!(
tag = ?Tag::EndRequest,
status_code = response_code,
time_taken_ms = request_duration.as_millis(),
);
res
}
pub fn log_and_return_error_response<T>(error: Report<T>) -> HttpResponse
where
T: actix_web::ResponseError + error_stack::Context + Clone,
{
logger::error!(?error);
HttpResponse::from_error(error.current_context().clone())
}
pub async fn authenticate_by_api_key(
store: &dyn StorageInterface,
api_key: &str,
) -> RouterResult<storage::MerchantAccount> {
store
.find_merchant_account_by_api_key(api_key)
.await
.change_context(errors::ApiErrorResponse::Unauthorized)
.attach_printable("Merchant not authenticated")
}
pub fn http_response_json<T: body::MessageBody + 'static>(response: T) -> HttpResponse {
HttpResponse::Ok()
.content_type("application/json")
.append_header((header::VIA, "Juspay_router"))
.append_header((header::STRICT_TRANSPORT_SECURITY, consts::HSTS_HEADER_VALUE))
.body(response)
}
pub fn http_response_plaintext<T: body::MessageBody + 'static>(res: T) -> HttpResponse {
HttpResponse::Ok()
.content_type("text/plain")
.append_header((header::VIA, "Juspay_router"))
.append_header((header::STRICT_TRANSPORT_SECURITY, consts::HSTS_HEADER_VALUE))
.body(res)
}
pub fn http_response_ok() -> HttpResponse {
HttpResponse::Ok()
.append_header((header::VIA, "Juspay_router"))
.append_header((header::STRICT_TRANSPORT_SECURITY, consts::HSTS_HEADER_VALUE))
.finish()
}
pub fn http_redirect_response<T: body::MessageBody + 'static>(
response: T,
redirection_response: api::RedirectionResponse,
) -> HttpResponse {
HttpResponse::Ok()
.content_type("application/json")
.append_header((header::VIA, "Juspay_router"))
.append_header((
"Location",
redirection_response.return_url_with_query_params,
))
.append_header((header::STRICT_TRANSPORT_SECURITY, consts::HSTS_HEADER_VALUE))
.status(http::StatusCode::FOUND)
.body(response)
}
pub fn http_response_err<T: body::MessageBody + 'static>(response: T) -> HttpResponse {
HttpResponse::BadRequest()
.content_type("application/json")
.append_header((header::VIA, "Juspay_router"))
.append_header((header::STRICT_TRANSPORT_SECURITY, consts::HSTS_HEADER_VALUE))
.body(response)
}
pub trait ConnectorRedirectResponse {
fn get_flow_type(
&self,
_query_params: &str,
) -> CustomResult<payments::CallConnectorAction, errors::ConnectorError> {
Ok(payments::CallConnectorAction::Avoid)
}
}
pub trait Authenticate {
fn get_client_secret(&self) -> Option<&String>;
}
impl Authenticate for api_models::payments::PaymentsRequest {
fn get_client_secret(&self) -> Option<&String> {
self.client_secret.as_ref()
}
}
impl Authenticate for api_models::payment_methods::PaymentMethodListRequest {
fn get_client_secret(&self) -> Option<&String> {
self.client_secret.as_ref()
}
}
pub fn build_redirection_form(form: &RedirectForm) -> maud::Markup {
use maud::PreEscaped;
maud::html! {
(maud::DOCTYPE)
html {
meta name="viewport" content="width=device-width, initial-scale=1";
head {
style {
r##"
"##
}
(PreEscaped(r##"
<style>
#loader1 {
width: 500px,
}
@media max-width: 600px {
#loader1 {
width: 200px
}
}
</style>
"##))
}
body style="background-color: #ffffff; padding: 20px; font-family: Arial, Helvetica, Sans-Serif;" {
div id="loader1" class="lottie" style="height: 150px; display: block; position: relative; margin-left: auto; margin-right: auto;" { "" }
(PreEscaped(r#"<script src="https://cdnjs.cloudflare.com/ajax/libs/bodymovin/5.7.4/lottie.min.js"></script>"#))
(PreEscaped(r#"
<script>
var anime = bodymovin.loadAnimation({
container: document.getElementById('loader1'),
renderer: 'svg',
loop: true,
autoplay: true,
name: 'hyperswitch loader',
animationData: {"v":"4.8.0","meta":{"g":"LottieFiles AE 3.1.1","a":"","k":"","d":"","tc":""},"fr":29.9700012207031,"ip":0,"op":31.0000012626559,"w":400,"h":250,"nm":"loader_shape","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"circle 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[278.25,202.671,0],"ix":2},"a":{"a":0,"k":[23.72,23.72,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[12.935,0],[0,-12.936],[-12.935,0],[0,12.935]],"o":[[-12.952,0],[0,12.935],[12.935,0],[0,-12.936]],"v":[[0,-23.471],[-23.47,0.001],[0,23.471],[23.47,0.001]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.427451010311,0.976470648074,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":19.99,"s":[100]},{"t":29.9800012211104,"s":[10]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.72,23.721],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48.0000019550801,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"square 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[196.25,201.271,0],"ix":2},"a":{"a":0,"k":[22.028,22.03,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.914,0],[0,0],[0,-1.914],[0,0],[-1.914,0],[0,0],[0,1.914],[0,0]],"o":[[0,0],[-1.914,0],[0,0],[0,1.914],[0,0],[1.914,0],[0,0],[0,-1.914]],"v":[[18.313,-21.779],[-18.312,-21.779],[-21.779,-18.313],[-21.779,18.314],[-18.312,21.779],[18.313,21.779],[21.779,18.314],[21.779,-18.313]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.427451010311,0.976470648074,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":5,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":14.99,"s":[100]},{"t":24.9800010174563,"s":[10]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[22.028,22.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":47.0000019143492,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Triangle 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[116.25,200.703,0],"ix":2},"a":{"a":0,"k":[27.11,21.243,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.558,-0.879],[0,0],[-1.133,0],[0,0],[0.609,0.947],[0,0]],"o":[[-0.558,-0.879],[0,0],[-0.609,0.947],[0,0],[1.133,0],[0,0],[0,0]],"v":[[1.209,-20.114],[-1.192,-20.114],[-26.251,18.795],[-25.051,20.993],[25.051,20.993],[26.251,18.795],[1.192,-20.114]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.427451010311,0.976470648074,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":9.99,"s":[100]},{"t":19.9800008138021,"s":[10]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[27.11,21.243],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48.0000019550801,"st":0,"bm":0}],"markers":[]}
})
</script>
"#))
h3 style="text-align: center;" { "Please wait while we process your payment..." }
form action=(PreEscaped(&form.endpoint)) method=(form.method.to_string()) #payment_form {
@for (field, value) in &form.form_fields {
input type="hidden" name=(field) value=(value);
}
}
(PreEscaped(r#"<script type="text/javascript"> var frm = document.getElementById("payment_form"); window.setTimeout(function () { frm.submit(); }, 300); </script>"#))
}
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_mime_essence() {
assert_eq!(mime::APPLICATION_JSON.essence_str(), "application/json");
}
}