tinkr 0.0.43

Tinkr is a web framework for quickly building full-stack web applications with Leptos.
Documentation
use crate::{
    RecordId,
    components::{heading::Heading, label::Label},
};
use leptos::prelude::*;
use serde::{Deserialize, Serialize};

/// Extra payment details from PayFast
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PaymentExtra {
    pub amount_fee: String,
    pub amount_gross: String,
    pub amount_net: String,
    pub custom_int1: String,
    pub custom_int2: String,
    pub custom_int3: String,
    pub custom_int4: String,
    pub custom_int5: String,
    pub custom_str1: String,
    pub custom_str2: String,
    pub custom_str3: String,
    pub custom_str4: String,
    pub custom_str5: String,
    pub email_address: String,
    pub item_description: String,
    pub item_name: String,
    pub m_payment_id: String,
    pub merchant_id: String,
    pub name_first: String,
    pub name_last: String,
    pub payment_status: String,
    pub pf_payment_id: String,
    pub signature: String,
}

/// Payment record from database
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Payment {
    pub id: RecordId,
    pub order: RecordId,
    pub address: String,
    pub amount_fee: String,
    pub amount_gross: String,
    pub amount_net: String,
    pub city: String,
    pub email_address: String,
    pub extra: PaymentExtra,
    pub item_description: String,
    pub item_name: String,
    pub m_payment_id: String,
    pub merchant_id: String,
    pub name_first: String,
    pub name_last: String,
    pub payment_status: String,
    pub pf_payment_id: String,
    #[serde(rename = "phoneNumber")]
    pub phone_number: String,
    pub postal_code: String,
    pub signature: String,
}

/// Fetch a payment from the database by order RecordId
#[cfg(feature = "ssr")]
pub async fn get_payment_by_order(order_id: RecordId) -> Result<Option<Payment>, ServerFnError> {
    use crate::db_init;

    let db = db_init().await?;

    let mut query = db
        .query("SELECT * FROM payments WHERE order = $order_id LIMIT 1")
        .bind(("order_id", order_id))
        .await?;

    let payment: Option<Payment> = query.take(0)?;

    Ok(payment)
}

#[cfg(feature = "ssr")]
pub async fn get_payment_by_id(payment_id: RecordId) -> Result<Option<Payment>, ServerFnError> {
    use crate::db_init;

    let db = db_init().await?;

    let payment: Option<Payment> = db.select(payment_id).await?;

    Ok(payment)
}

// #[cfg(feature = "ssr")]
// #[cfg(test)]
// mod tests {
//     use super::*;
//     use crate::RecordId;

//     #[tokio::test]
//     #[ignore] // Remove this to run the test with a real database
//     async fn test_get_payment_by_order() -> Result<(), ServerFnError> {

//         let order_id = RecordId::from_table_key("order", "2ygpmy9d1nr00vksova8");
//         let payment = get_payment_by_order(order_id).await?;

//         assert!(payment.is_some());
//         if let Some(payment) = payment {
//             assert_eq!(payment.payment_status, "COMPLETE");
//             assert_eq!(payment.item_name, "ScratchFixPro");
//         }

//         Ok(())
//     }
// }

#[component]
pub fn PaymentView(payment: Payment) -> impl IntoView {
    view! {
        <div class="pt-5">
            <Heading>"Payment Details"</Heading>

            <section class="grid grid-cols-2 gap-5 mt-5">
                <div>
                    <Label>"Name"</Label>
                    <span>{payment.name_first}" "{payment.name_last}</span>
                </div>

                <div>
                    <Label>"Phone"</Label>
                    <span>{payment.phone_number}</span>
                </div>

                <div>
                    <Label>"Address"</Label>
                    <span>{payment.address}</span>
                    <br />
                    <span>{payment.city}</span>
                    <br />
                    <span>{payment.postal_code}</span>
                </div>

            </section>
        </div>
    }
    .into_view()
}

use crate::order::order_model::OrderFull;
use crate::payments::payfast::PayFastButton;

#[component]
pub fn PaymentOptions(order: OrderFull) -> impl IntoView {
    let Some(delivery_details) = order.delivery_details.clone() else {
        return view! { <div>"No delivery details found for this order."</div> }.into_any();
    };

    view! {
        <div>
            <div class="flex flex-col gap-4">

                <PayFastButton
                    payment_order_title="ScratchFixPro".to_string()
                    payment_order_description="PaintKit Order".to_string()
                    payment_uuid=order.id.to_string().clone()
                    payment_first_name=delivery_details.first_name.unwrap_or_default()
                    payment_last_name=delivery_details.last_name.unwrap_or_default()
                    payment_email=delivery_details.email.to_string()
                    payment_telephone=delivery_details
                        .phone
                        .unwrap_or(delivery_details.telephone.unwrap_or_default())
                    payment_confirm_amount=order.total.parse().unwrap_or(0.0)
                    address=delivery_details.address1.unwrap_or_default()
                    city=delivery_details.address2.unwrap_or_default()
                    province=delivery_details.address3.unwrap_or_default()
                    postal_code=delivery_details.postcode.unwrap_or_default()
                />
            // sandbox=true

            // <pre>{serde_json::to_string_pretty(&order).unwrap()}</pre>

            // <Button color=BtnColor::Primary>"Pay with Peach Payments"</Button>
            </div>
        </div>
    }
    .into_any()
}

use crate::components::Button;
use leptos::prelude::*;
use leptos_router::hooks::use_params_map;
use phosphor_leptos::Icon;

#[server]
async fn fetch_payment(payment_id: RecordId) -> Result<Payment, ServerFnError> {
    let payment = crate::payments::payment::get_payment_by_order(payment_id).await?;

    println!("Fetched payment: {:?}", payment);

    let payment = match payment {
        Some(p) => p,
        None => return Err(ServerFnError::ServerError("Payment not found".into())),
    };

    Ok(payment)
}

#[component]
pub fn PaymentStatusPage() -> impl IntoView {
    let params = use_params_map();
    let paymentid = move || params.read().get("paymentid").unwrap_or_default();
    let status = move || params.read().get("status").unwrap_or_default();

    // let order_resource = Resource::new(
    //     move || {
    //         let key = paymentid();
    //         let out: RecordId = RecordId::from_table_key("order", &key);
    //         out
    //     },
    //     fetch_payment,
    // );

    view! {
        <div class="p-20 text-center h-[75vh] flex flex-col justify-center items-center">
            <h1 class="text-center text-4xl">"Payment Status "{status().to_uppercase()}</h1>

            {match status().as_str() {
                "success" => {
                    view! {
                        <div class="mx-auto mt-4 text-green-500 flex justify-center items-center gap-2">
                            <Icon icon=phosphor_leptos::CHECK_CIRCLE size="64px" />
                        </div>
                    }
                }
                "failed" => {
                    view! {
                        <div class="mx-auto mt-4 text-red-500 flex justify-center items-center gap-2">
                            <Icon icon=phosphor_leptos::X_CIRCLE size="64px" />
                        </div>
                    }
                }
                _ => {
                    view! {
                        <div class="mx-auto mt-4 text-red-500 flex justify-center items-center gap-2">
                            <Icon icon=phosphor_leptos::X_CIRCLE size="64px" />
                        </div>
                    }
                }
            }}

            <Button href=format!("/order/{}", paymentid()) class="mt-8">
                "View Order Details"
            </Button>

        // <Transition>
        // {move || {
        // order_resource
        // .get()
        // .map(|payment_result| {
        // match payment_result {
        // Ok(payment) => {
        // view! {
        // <div class="mt-4 p-4 border rounded bg-neutral-50 dark:bg-neutral-800">
        // <h2 class="text-xl font-semibold mb-2">
        // "Payment Details"
        // </h2>
        // <pre>{serde_json::to_string_pretty(&payment).unwrap()}</pre>
        // <span>"Thank you for your payment!"</span>
        // </div>
        // }
        // .into_any()
        // }
        // Err(_) => {
        // view! { <p>"Failed to load payment details."</p> }.into_any()
        // }
        // }
        // })
        // }}
        // </Transition>
        </div>
    }
}