Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add customers rest api #436

Merged
merged 13 commits into from
Dec 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci-rust.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ jobs:
run: |
cargo run -p meteroid --bin openapi-generate
if [[ -n "$(git status --porcelain spec/api/v1/openapi.json)" ]]; then
echo "openapi.json is not up to date. Please run `cargo run -p meteroid --bin openapi-generate` and commit changes."
echo "openapi.json is not up to date. Please run 'cargo run -p meteroid --bin openapi-generate' and commit changes."
git --no-pager diff spec/api/v1/openapi.json
exit 1
fi
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ typetag = "0.2.13"
unic-langid = "0.9"
url = "2.4.1"
uuid = "1.4.1"
utoipa = { version = "5.2.0", features = ["axum_extras", "uuid", "debug", "chrono"] }
utoipa-axum = { version = "0.1.2" }
utoipa = { version = "5.3.0", features = ["axum_extras", "uuid", "debug", "chrono"] }
utoipa-axum = { version = "0.1.3" }
utoipa-swagger-ui = { version = "8.0.3", features = ["axum"] }
utoipa-redoc = { version = "5.0.0", features = ["axum"] }
utoipa-rapidoc = { version = "5.0.0", features = ["axum"] }
Expand Down
1 change: 1 addition & 0 deletions modules/meteroid/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ utoipa-rapidoc.workspace = true
utoipa-scalar.workspace = true

svix = { workspace = true, features = ["http2", "rustls-tls"] }
strum = { workspace = true, features = ["derive"] }

rdkafka = { workspace = true }
kafka = { workspace = true }
Expand Down
28 changes: 28 additions & 0 deletions modules/meteroid/crates/diesel-models/src/customers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,34 @@ pub struct CustomerRow {
pub local_id: String,
}

#[derive(Clone, Debug, Identifiable, Queryable, Selectable)]
#[diesel(table_name = crate::schema::customer)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct CustomerForDisplayRow {
pub id: Uuid,
pub name: String,
pub created_at: NaiveDateTime,
pub created_by: Uuid,
pub updated_at: Option<NaiveDateTime>,
pub updated_by: Option<Uuid>,
pub archived_at: Option<NaiveDateTime>,
pub tenant_id: Uuid,
pub billing_config: serde_json::Value,
pub alias: Option<String>,
pub email: Option<String>,
pub invoicing_email: Option<String>,
pub phone: Option<String>,
pub balance_value_cents: i32,
pub currency: String,
pub billing_address: Option<serde_json::Value>,
pub shipping_address: Option<serde_json::Value>,
pub invoicing_entity_id: Uuid,
pub local_id: String,
#[diesel(select_expression = crate::schema::invoicing_entity::local_id)]
#[diesel(select_expression_type = crate::schema::invoicing_entity::local_id)]
pub invoicing_entity_local_id: String,
}

#[derive(Clone, Debug, Queryable, Selectable)]
#[diesel(table_name = crate::schema::customer)]
#[diesel(check_for_backend(diesel::pg::Pg))]
Expand Down
98 changes: 92 additions & 6 deletions modules/meteroid/crates/diesel-models/src/query/customers.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use crate::customers::{CustomerBriefRow, CustomerRow, CustomerRowNew, CustomerRowPatch};
use crate::customers::{
CustomerBriefRow, CustomerForDisplayRow, CustomerRow, CustomerRowNew, CustomerRowPatch,
};
use crate::errors::IntoDbResult;
use crate::extend::order::OrderByRequest;
use crate::extend::pagination::{Paginate, PaginatedVec, PaginationRequest};
use crate::query::IdentityDb;
use crate::{DbResult, PgConn};
use diesel::{
debug_query, BoolExpressionMethods, ExpressionMethods, OptionalExtension,
debug_query, BoolExpressionMethods, ExpressionMethods, JoinOnDsl, OptionalExtension,
PgTextExpressionMethods, QueryDsl, SelectableHelper,
};
use error_stack::ResultExt;
Expand All @@ -31,15 +34,23 @@ impl CustomerRowNew {
impl CustomerRow {
pub async fn find_by_id(
conn: &mut PgConn,
customer_id: Uuid,
customer_id: IdentityDb,
tenant_id_param: Uuid,
) -> DbResult<CustomerRow> {
use crate::schema::customer::dsl::*;
use diesel_async::RunQueryDsl;

let query = customer
.filter(id.eq(customer_id))
.filter(tenant_id.eq(tenant_id_param));
let mut query = customer.filter(tenant_id.eq(tenant_id_param)).into_boxed();

match customer_id {
IdentityDb::UUID(id_param) => {
query = query.filter(id.eq(id_param));
}
IdentityDb::LOCAL(local_id_param) => {
query = query.filter(local_id.eq(local_id_param));
}
}

log::debug!("{}", debug_query::<diesel::pg::Pg, _>(&query).to_string());

query
Expand Down Expand Up @@ -230,3 +241,78 @@ impl CustomerRowPatch {
.into_db_result()
}
}

impl CustomerForDisplayRow {
pub async fn find_by_local_id_or_alias(
conn: &mut PgConn,
tenant_id: Uuid,
local_id_or_alias: String,
) -> DbResult<CustomerForDisplayRow> {
use crate::schema::customer::dsl as c_dsl;
use crate::schema::invoicing_entity::dsl as ie_dsl;
use diesel_async::RunQueryDsl;

let query = c_dsl::customer
.filter(c_dsl::tenant_id.eq(tenant_id))
.filter(
c_dsl::local_id
.eq(local_id_or_alias.as_str())
.or(c_dsl::alias.eq(local_id_or_alias.as_str())),
)
.inner_join(ie_dsl::invoicing_entity.on(c_dsl::invoicing_entity_id.eq(ie_dsl::id)))
.select(CustomerForDisplayRow::as_select());
log::debug!("{}", debug_query::<diesel::pg::Pg, _>(&query).to_string());

query
.first(conn)
.await
.attach_printable("Error while finding customer by local_id or alias")
.into_db_result()
}

pub async fn list(
conn: &mut PgConn,
param_tenant_id: Uuid,
pagination: PaginationRequest,
order_by: OrderByRequest,
param_query: Option<String>,
) -> DbResult<PaginatedVec<CustomerForDisplayRow>> {
use crate::schema::customer::dsl as c_dsl;
use crate::schema::invoicing_entity::dsl as ie_dsl;

let mut query = c_dsl::customer
.filter(c_dsl::tenant_id.eq(param_tenant_id))
.inner_join(ie_dsl::invoicing_entity.on(c_dsl::invoicing_entity_id.eq(ie_dsl::id)))
.select(CustomerForDisplayRow::as_select())
.into_boxed();

if let Some(param_query) = param_query {
query = query.filter(
c_dsl::name
.ilike(format!("%{}%", param_query))
.or(c_dsl::alias.ilike(format!("%{}%", param_query))),
);
}

match order_by {
OrderByRequest::IdAsc => query = query.order(c_dsl::id.asc()),
OrderByRequest::IdDesc => query = query.order(c_dsl::id.desc()),
OrderByRequest::DateAsc => query = query.order(c_dsl::created_at.asc()),
OrderByRequest::DateDesc => query = query.order(c_dsl::created_at.desc()),
_ => query = query.order(c_dsl::id.asc()),
}

let paginated_query = query.paginate(pagination);

log::debug!(
"{}",
debug_query::<diesel::pg::Pg, _>(&paginated_query).to_string()
);

paginated_query
.load_and_count_pages(conn)
.await
.attach_printable("Error while fetching customers")
.into_db_result()
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::errors::IntoDbResult;
use crate::invoicing_entities::{InvoicingEntityRow, InvoicingEntityRowPatch};
use crate::query::IdentityDb;

use crate::{DbResult, PgConn};

Expand Down Expand Up @@ -129,16 +130,25 @@ impl InvoicingEntityRow {

pub async fn get_invoicing_entity_by_id_and_tenant(
conn: &mut PgConn,
id: &uuid::Uuid,
id: &IdentityDb,
tenant_id: &uuid::Uuid,
) -> DbResult<InvoicingEntityRow> {
use crate::schema::invoicing_entity::dsl;
use diesel_async::RunQueryDsl;

let query = dsl::invoicing_entity
.filter(dsl::id.eq(id))
let mut query = dsl::invoicing_entity
.filter(dsl::tenant_id.eq(tenant_id))
.select(InvoicingEntityRow::as_select());
.select(InvoicingEntityRow::as_select())
.into_boxed();

match id {
IdentityDb::UUID(id_param) => {
query = query.filter(dsl::id.eq(id_param));
}
IdentityDb::LOCAL(local_id_param) => {
query = query.filter(dsl::local_id.eq(local_id_param));
}
}

log::debug!("{}", debug_query::<diesel::pg::Pg, _>(&query).to_string());

Expand Down
71 changes: 65 additions & 6 deletions modules/meteroid/crates/meteroid-store/src/domain/customers.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
use crate::domain::Identity;
use crate::errors::StoreError;
use crate::utils::local_id::{IdType, LocalId};
use chrono::NaiveDateTime;
use diesel_models::customers::CustomerRow;
use diesel_models::customers::{CustomerBriefRow, CustomerRowNew, CustomerRowPatch};
use diesel_models::customers::{CustomerForDisplayRow, CustomerRow};
use error_stack::Report;
use o2o::o2o;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;

use crate::errors::StoreError;
use crate::utils::local_id::{IdType, LocalId};

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Customer {
pub id: Uuid,
Expand Down Expand Up @@ -113,7 +113,7 @@ pub struct CustomerNew {
pub shipping_address: Option<ShippingAddress>,
//
pub created_by: Uuid,
pub invoicing_entity_id: Option<Uuid>,
pub invoicing_entity_id: Option<Identity>,
// for seeding
pub force_created_date: Option<chrono::NaiveDateTime>,
}
Expand Down Expand Up @@ -260,7 +260,13 @@ pub enum BillingConfig {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Stripe {
pub customer_id: String,
pub collection_method: i32, // todo fix: models.proto : CollectionMethod
pub collection_method: StripeCollectionMethod,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum StripeCollectionMethod {
ChargeAutomatically,
SendInvoice,
}

impl TryFrom<serde_json::Value> for BillingConfig {
Expand Down Expand Up @@ -307,3 +313,56 @@ pub struct CustomerBuyCredits {
pub cents: i32,
pub notes: Option<String>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomerForDisplay {
pub id: Uuid,
pub local_id: String,
pub name: String,
pub created_at: NaiveDateTime,
pub created_by: Uuid,
pub updated_at: Option<NaiveDateTime>,
pub updated_by: Option<Uuid>,
pub archived_at: Option<NaiveDateTime>,
pub tenant_id: Uuid,
pub invoicing_entity_id: Uuid,
pub invoicing_entity_local_id: String,
pub billing_config: BillingConfig,
pub alias: Option<String>,
pub email: Option<String>,
pub invoicing_email: Option<String>,
pub phone: Option<String>,
pub balance_value_cents: i32,
pub currency: String,
pub billing_address: Option<Address>,
pub shipping_address: Option<ShippingAddress>,
}

impl TryFrom<CustomerForDisplayRow> for CustomerForDisplay {
type Error = Report<StoreError>;

fn try_from(value: CustomerForDisplayRow) -> Result<Self, Self::Error> {
Ok(CustomerForDisplay {
id: value.id,
local_id: value.local_id,
name: value.name,
created_at: value.created_at,
created_by: value.created_by,
updated_at: value.updated_at,
updated_by: value.updated_by,
archived_at: value.archived_at,
tenant_id: value.tenant_id,
billing_config: value.billing_config.try_into()?,
alias: value.alias,
email: value.email,
invoicing_email: value.invoicing_email,
phone: value.phone,
balance_value_cents: value.balance_value_cents,
currency: value.currency,
billing_address: value.billing_address.map(|v| v.try_into()).transpose()?,
shipping_address: value.shipping_address.map(|v| v.try_into()).transpose()?,
invoicing_entity_id: value.invoicing_entity_id,
invoicing_entity_local_id: value.invoicing_entity_local_id,
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::StoreResult;
use diesel_models::customer_balance_txs::CustomerBalanceTxRowNew;
use diesel_models::customers::CustomerRow;
use diesel_models::errors::DatabaseError;
use diesel_models::query::IdentityDb;
use error_stack::Report;
use uuid::Uuid;

Expand Down Expand Up @@ -36,9 +37,10 @@ impl CustomerBalance {
_ => Into::<Report<StoreError>>::into(err),
})?;

let customer_row_updated = CustomerRow::find_by_id(conn, customer_id, tenant_id)
.await
.map_err(Into::<Report<StoreError>>::into)?;
let customer_row_updated =
CustomerRow::find_by_id(conn, IdentityDb::UUID(customer_id), tenant_id)
.await
.map_err(Into::<Report<StoreError>>::into)?;

let tx = CustomerBalanceTxRowNew {
id: Uuid::now_v7(),
Expand Down
Loading
Loading