feat: bootstrap add bill web interface
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful

This commit is contained in:
Aravinth Manivannan 2024-12-18 18:14:29 +05:30
parent f8870594ce
commit 92a22100be
Signed by: realaravinth
GPG key ID: F8F50389936984FF
7 changed files with 341 additions and 2 deletions

View file

@ -1,3 +1,4 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
pub mod web;

View file

@ -0,0 +1,168 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use actix_identity::Identity;
use actix_web::{get, http::header::ContentType, post, web, HttpRequest, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use url::Url;
use uuid::Uuid;
use super::errors::*;
use super::types;
use crate::billing::domain::add_store_command::AddStoreCommandBuilder;
use crate::billing::domain::{add_bill_command::*, bill_updated_event, commands::BillingCommand};
use crate::utils::uuid::WebGetUUIDInterfaceObj;
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(add_bill_submit_handler);
cfg.service(add_bill_page_handler);
cfg.service(add_store_page_handler);
cfg.service(add_store_submit_handler);
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
struct WebAddBillPayload {
store_id: Uuid,
}
#[allow(clippy::too_many_arguments)]
#[get("/billing/store/add")]
#[tracing::instrument(name = "add store page handler", skip())]
//async fn add_bill_page_handler(_: Identity) -> WebJsonRepsonse<impl Responder> {
async fn add_store_page_handler() -> WebJsonRepsonse<impl Responder> {
let page = r#"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<form action="/billing/store/add" method="post">
<button type="submit">
Create Store
</button>
</form>
</body>
</html>
"#;
Ok(HttpResponse::Ok()
.insert_header(ContentType::html())
.body(page))
}
const UUID: Uuid = uuid::uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8");
#[allow(clippy::too_many_arguments)]
#[post("/billing/store/add")]
#[tracing::instrument(
name = "add store handler",
skip(billing_store_cqrs_exec, billing_store_cqrs_view, uuid_generator)
)]
async fn add_store_submit_handler(
billing_store_cqrs_exec: types::WebBillingStoreCqrsExec,
billing_store_cqrs_view: types::WebBillingStoreCqrsView,
uuid_generator: WebGetUUIDInterfaceObj,
req: HttpRequest,
// id: Identity,
) -> WebJsonRepsonse<impl Responder> {
// let user_id = Uuid::parse_str(&id.id().unwrap()).unwrap();
let user_id = UUID;
let store_uuid = UUID;
let store_uuid_str = store_uuid.to_string();
let cmd = AddStoreCommandBuilder::default()
.name("foo".into())
.owner(user_id)
.store_id(UUID)
.address(None)
.build()
.unwrap();
billing_store_cqrs_exec
.execute(&store_uuid_str, BillingCommand::AddStore(cmd))
.await
.unwrap();
let store = billing_store_cqrs_view
.load(&store_uuid_str)
.await
.unwrap()
.unwrap();
Ok(HttpResponse::Ok().json(store))
}
#[allow(clippy::too_many_arguments)]
#[get("/billing/bill/add")]
#[tracing::instrument(name = "add bill page handler", skip())]
//async fn add_bill_page_handler(_: Identity) -> WebJsonRepsonse<impl Responder> {
async fn add_bill_page_handler() -> WebJsonRepsonse<impl Responder> {
let page = r#"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
Token Refreshed
<form action="/billing/bill/add" method="post">
<label> Store ID <input type="text" name="store_id" id="store_id" /></label>
<button type="submit">
Refresh Token
</button>
</form>
</body>
</html>
"#;
Ok(HttpResponse::Ok()
.insert_header(ContentType::html())
.body(page))
}
#[allow(clippy::too_many_arguments)]
#[post("/billing/bill/add")]
#[tracing::instrument(
name = "add bill handler",
skip(billing_bill_cqrs_exec, billing_bill_cqrs_view,)
)]
async fn add_bill_submit_handler(
billing_bill_cqrs_exec: types::WebBillingBillCqrsExec,
billing_bill_cqrs_view: types::WebBillingBillCqrsView,
req: HttpRequest,
// id: Identity,
payload: web::Form<WebAddBillPayload>,
) -> WebJsonRepsonse<impl Responder> {
// let user_id = Uuid::parse_str(&id.id().unwrap()).unwrap();
let user_id = Uuid::new_v4();
let bill_uuid = Uuid::new_v4();
let bill_uuid_str = bill_uuid.to_string();
let cmd = AddBillCommandBuilder::default()
.adding_by(user_id)
.bill_id(bill_uuid)
.store_id(payload.store_id)
.build()
.unwrap();
billing_bill_cqrs_exec
.execute(&bill_uuid_str, BillingCommand::AddBill(cmd))
.await
.unwrap();
let bill = billing_bill_cqrs_view
.load(&bill_uuid_str)
.await
.unwrap()
.unwrap();
Ok(HttpResponse::Ok().json(bill))
}

View file

@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use actix_web::http::StatusCode;
use actix_web::{HttpResponse, ResponseError};
use derive_more::Display;
use serde::{Deserialize, Serialize};
use crate::billing::application::services::errors::*;
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
struct ErrorResponse {
error: String,
}
impl From<WebError> for ErrorResponse {
fn from(value: WebError) -> Self {
ErrorResponse {
error: serde_json::to_string(&value).unwrap_or_else(|_| {
log::error!("Unable to serialize error");
"Unable to serialize error".into()
}),
}
}
}
#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum WebError {
InternalError,
BillNotFound,
StoreNotFound,
LineItemNotFound,
BadRequest,
}
impl From<BillingError> for WebError {
fn from(v: BillingError) -> Self {
match v {
BillingError::BillIDNotFound => Self::BillNotFound,
BillingError::InternalError => Self::InternalError,
BillingError::DuplicateStoreName => Self::InternalError,
BillingError::DuplicateBillID => Self::InternalError,
BillingError::DuplicateLineItemID => Self::InternalError,
BillingError::DuplicateStoreID => Self::InternalError,
BillingError::StoreIDNotFound => Self::StoreNotFound,
BillingError::LineItemIDNotFound => Self::LineItemNotFound,
}
}
}
impl ResponseError for WebError {
fn status_code(&self) -> StatusCode {
match self {
Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
Self::StoreNotFound => StatusCode::NOT_FOUND,
Self::LineItemNotFound => StatusCode::NOT_FOUND,
Self::BillNotFound => StatusCode::NOT_FOUND,
Self::BadRequest => StatusCode::BAD_REQUEST,
}
}
fn error_response(&self) -> actix_web::HttpResponse {
let e: ErrorResponse = self.clone().into();
match self {
Self::InternalError => HttpResponse::InternalServerError().json(e),
Self::StoreNotFound => HttpResponse::NotFound().json(e),
Self::LineItemNotFound => HttpResponse::NotFound().json(e),
Self::BillNotFound => HttpResponse::BadRequest().json(e),
Self::BadRequest => HttpResponse::BadRequest().json(e),
}
}
}
pub type WebJsonRepsonse<V> = Result<V, WebError>;

View file

@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::sync::Arc;
use actix_web::web;
use crate::billing::adapters::types;
mod bill;
mod errors;
mod routes;
pub use errors::WebJsonRepsonse;
pub use routes::RoutesRepository;
pub fn load_ctx() -> impl FnOnce(&mut web::ServiceConfig) {
let routes = types::WebBillingRoutesRepository::new(Arc::new(RoutesRepository::default()));
let f = move |cfg: &mut web::ServiceConfig| {
cfg.app_data(routes);
cfg.configure(bill::services);
};
Box::new(f)
}

View file

@ -0,0 +1,67 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use url::Url;
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct RoutesRepository {
add_bill: String,
update_bill: String,
delete_bill: String,
compute_total_price_for_bill: String,
add_line_item: String,
update_line_item: String,
delete_line_item: String,
}
impl Default for RoutesRepository {
fn default() -> Self {
Self {
add_bill: "/billing/bill/add".into(),
update_bill: "/billing/bill/{bill_uuid}".into(),
delete_bill: "/billing/bill/{bill_uuid}".into(),
compute_total_price_for_bill: "/billing/bill/{bill_uuid}/compute-total".into(),
add_line_item: "/billing/bill/{bill_id}/line_item/add".into(),
update_line_item: "/billing/bill/{bill_id}/line_item/{line_item_uuid}".into(),
delete_line_item: "/billing/bill/{bill_id}/line_item/{line_item_uuid}".into(),
}
}
}
impl RoutesRepository {
pub fn update_bill(&self, bill_id: Uuid) -> String {
self.update_bill
.replace("{bill_uuid}", &bill_id.to_string())
}
pub fn delete_bill(&self, bill_id: Uuid) -> String {
self.delete_bill
.replace("{bill_uuid}", &bill_id.to_string())
}
pub fn compute_total_price_for_bill(&self, bill_id: Uuid) -> String {
self.compute_total_price_for_bill
.replace("{bill_uuid}", &bill_id.to_string())
}
pub fn add_line_item(&self, bill_id: Uuid) -> String {
self.add_line_item
.replace("{bill_uuid}", &bill_id.to_string())
}
pub fn update_line_item(&self, bill_id: Uuid, line_item_uuid: Uuid) -> String {
self.update_line_item
.replace("{bill_uuid}", &bill_id.to_string())
.replace("{line_item_uuid}", &line_item_uuid.to_string())
}
pub fn delete_line_item(&self, bill_id: Uuid, line_item_uuid: Uuid) -> String {
self.delete_line_item
.replace("{bill_uuid}", &bill_id.to_string())
.replace("{line_item_uuid}", &line_item_uuid.to_string())
}
}

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
mod db;
pub(crate) mod db;

View file

@ -2,6 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
mod adapters;
pub(crate) mod adapters;
mod application;
mod domain;