feat: implement transaction service with CRUD operations and validation
This commit is contained in:
381
src-tauri/src/services/transaction/dto.rs
Normal file
381
src-tauri/src/services/transaction/dto.rs
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
// TODO: validation for all dto
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
db::{
|
||||||
|
DbServiceError,
|
||||||
|
entities::{account::Column::Id, transaction::Model as TransactionModel},
|
||||||
|
},
|
||||||
|
services::transaction::{category::CategoryServiceError, tag::TagServiceError},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::db::{
|
||||||
|
Db,
|
||||||
|
entities::transaction::{ActiveModel as TransactionActiveModel, Column::AccountId},
|
||||||
|
};
|
||||||
|
use sea_orm::{ActiveValue::Set, ColumnTrait, EntityTrait, QueryFilter};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum TransactionServiceError {
|
||||||
|
#[error("database error: {0}")]
|
||||||
|
DbErr(#[from] DbServiceError),
|
||||||
|
#[error("Target not found: {0}")]
|
||||||
|
TargetNotFound(String),
|
||||||
|
#[error("Validation error: {0}")]
|
||||||
|
Validation(String),
|
||||||
|
#[error("unknown error: {0}")]
|
||||||
|
Unknown(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<sea_orm::DbErr> for TransactionServiceError {
|
||||||
|
fn from(err: sea_orm::DbErr) -> Self {
|
||||||
|
TransactionServiceError::DbErr(DbServiceError::from(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TagServiceError> for TransactionServiceError {
|
||||||
|
fn from(err: TagServiceError) -> Self {
|
||||||
|
match err {
|
||||||
|
TagServiceError::DbErr(db_err) => TransactionServiceError::DbErr(db_err),
|
||||||
|
TagServiceError::TargetNotFound(msg) => TransactionServiceError::TargetNotFound(msg),
|
||||||
|
TagServiceError::Validation(msg) => TransactionServiceError::Validation(msg),
|
||||||
|
TagServiceError::Unknown(msg) => TransactionServiceError::Unknown(msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CategoryServiceError> for TransactionServiceError {
|
||||||
|
fn from(err: CategoryServiceError) -> Self {
|
||||||
|
match err {
|
||||||
|
CategoryServiceError::DbErr(db_err) => TransactionServiceError::DbErr(db_err),
|
||||||
|
CategoryServiceError::TargetNotFound(msg) => {
|
||||||
|
TransactionServiceError::TargetNotFound(msg)
|
||||||
|
}
|
||||||
|
CategoryServiceError::Validation(msg) => TransactionServiceError::Validation(msg),
|
||||||
|
CategoryServiceError::Unknown(msg) => TransactionServiceError::Unknown(msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum TransactionType {
|
||||||
|
#[serde(rename = "STANDARD")]
|
||||||
|
Standard,
|
||||||
|
#[serde(rename = "RECONCILIATION")]
|
||||||
|
Reconciliation,
|
||||||
|
#[serde(rename = "OPENING_BALANCE")]
|
||||||
|
OpeningBalance,
|
||||||
|
#[serde(other, rename = "UNKNOWN")]
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for TransactionType {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
match serde_json::from_str::<TransactionType>(&format!("\"{}\"", s)) {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(_) => TransactionType::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for TransactionType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let s = match self {
|
||||||
|
TransactionType::Standard => "STANDARD",
|
||||||
|
TransactionType::Reconciliation => "RECONCILIATION",
|
||||||
|
TransactionType::OpeningBalance => "OPENING_BALANCE",
|
||||||
|
TransactionType::Unknown => "UNKNOWN",
|
||||||
|
};
|
||||||
|
write!(f, "{}", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Transaction {
|
||||||
|
pub id: i64,
|
||||||
|
pub account_id: i64,
|
||||||
|
pub amount: String,
|
||||||
|
pub transaction_type: TransactionType,
|
||||||
|
pub currency_code: String,
|
||||||
|
pub exchange_rate: Option<String>,
|
||||||
|
pub transaction_date: String,
|
||||||
|
pub metadata: TransactionMetadata,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
pub from_account_id: Option<i64>,
|
||||||
|
pub category_id: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TransactionModel> for Transaction {
|
||||||
|
fn from(model: TransactionModel) -> Self {
|
||||||
|
Self {
|
||||||
|
id: model.id,
|
||||||
|
account_id: model.account_id,
|
||||||
|
amount: model.amount,
|
||||||
|
transaction_type: model.transaction_type.into(),
|
||||||
|
currency_code: model.currency_code,
|
||||||
|
exchange_rate: model.exchange_rate,
|
||||||
|
transaction_date: model.transaction_date,
|
||||||
|
metadata: TransactionMetadata::from(model.metadata),
|
||||||
|
created_at: model.created_at.to_string(),
|
||||||
|
updated_at: model.updated_at.to_string(),
|
||||||
|
from_account_id: model.from_account_id,
|
||||||
|
category_id: model.category_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct TransactionMetadata {}
|
||||||
|
|
||||||
|
impl From<String> for TransactionMetadata {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
serde_json::from_str(&s).unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Option<String>> for TransactionMetadata {
|
||||||
|
fn from(s: Option<String>) -> Self {
|
||||||
|
s.map(|s| serde_json::from_str(&s).unwrap_or_default())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransactionMetadata {
|
||||||
|
pub fn validate(&self) -> Result<(), TransactionServiceError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn validate_account_exists<T>(
|
||||||
|
account_id: i64,
|
||||||
|
account_service: Arc<T>,
|
||||||
|
) -> Result<(), TransactionServiceError>
|
||||||
|
where
|
||||||
|
T: crate::services::accounts::AccountsService + ?Sized,
|
||||||
|
{
|
||||||
|
let account_exists = account_service
|
||||||
|
.get_account(&account_id)
|
||||||
|
.await
|
||||||
|
// TODO: add error from account service to error chain
|
||||||
|
.map_err(|err| TransactionServiceError::Unknown(err))?
|
||||||
|
.is_some();
|
||||||
|
if !account_exists {
|
||||||
|
return Err(TransactionServiceError::Validation(format!(
|
||||||
|
"Account with id {} does not exist",
|
||||||
|
account_id
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn validate_category_exists<T>(
|
||||||
|
category_id: i64,
|
||||||
|
category_service: Arc<T>,
|
||||||
|
) -> Result<(), TransactionServiceError>
|
||||||
|
where
|
||||||
|
T: crate::services::CategoryService + ?Sized,
|
||||||
|
{
|
||||||
|
let category_exists = category_service.get_category(category_id).await?.is_some();
|
||||||
|
if !category_exists {
|
||||||
|
return Err(TransactionServiceError::Validation(format!(
|
||||||
|
"Category with id {} does not exist",
|
||||||
|
category_id
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CreateTransactionRequest {
|
||||||
|
pub account_id: i64,
|
||||||
|
pub amount: String,
|
||||||
|
pub transaction_type: TransactionType,
|
||||||
|
pub currency_code: String,
|
||||||
|
pub exchange_rate: Option<String>,
|
||||||
|
pub transaction_date: String,
|
||||||
|
pub metadata: Option<TransactionMetadata>,
|
||||||
|
pub from_account_id: Option<i64>,
|
||||||
|
pub category_id: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CreateTransactionRequest {
|
||||||
|
pub async fn validate<T, U>(
|
||||||
|
&self,
|
||||||
|
account_service: Arc<T>,
|
||||||
|
category_service: Arc<U>,
|
||||||
|
) -> Result<(), TransactionServiceError>
|
||||||
|
where
|
||||||
|
T: crate::services::accounts::AccountsService + ?Sized,
|
||||||
|
U: crate::services::CategoryService + ?Sized,
|
||||||
|
{
|
||||||
|
if self.amount.trim().is_empty() {
|
||||||
|
return Err(TransactionServiceError::Validation(
|
||||||
|
"Amount cannot be empty".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// check if amount is a valid decimal
|
||||||
|
if self.amount.parse::<rust_decimal::Decimal>().is_err() {
|
||||||
|
return Err(TransactionServiceError::Validation(
|
||||||
|
"Amount must be a valid decimal".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if self.currency_code.trim().is_empty() {
|
||||||
|
return Err(TransactionServiceError::Validation(
|
||||||
|
"Currency code cannot be empty".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if self.transaction_date.trim().is_empty() {
|
||||||
|
return Err(TransactionServiceError::Validation(
|
||||||
|
"Transaction date cannot be empty".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// check if transaction_date is a valid date
|
||||||
|
if chrono::DateTime::parse_from_rfc3339(&self.transaction_date).is_err() {
|
||||||
|
return Err(TransactionServiceError::Validation(
|
||||||
|
"Transaction date must be a valid RFC3339 date".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(metadata) = &self.metadata {
|
||||||
|
metadata.validate()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_account_exists(self.account_id, account_service).await?;
|
||||||
|
if let Some(category_id) = self.category_id {
|
||||||
|
validate_category_exists(category_id, category_service).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CreateTransactionRequest> for TransactionActiveModel {
|
||||||
|
fn from(request: CreateTransactionRequest) -> Self {
|
||||||
|
Self {
|
||||||
|
account_id: Set(request.account_id),
|
||||||
|
amount: Set(request.amount),
|
||||||
|
transaction_type: Set(request.transaction_type.to_string()),
|
||||||
|
currency_code: Set(request.currency_code),
|
||||||
|
exchange_rate: Set(request.exchange_rate),
|
||||||
|
transaction_date: Set(request.transaction_date),
|
||||||
|
metadata: Set(request
|
||||||
|
.metadata
|
||||||
|
.map(|m| serde_json::to_string(&m).unwrap_or_default())),
|
||||||
|
from_account_id: Set(request.from_account_id),
|
||||||
|
category_id: Set(request.category_id),
|
||||||
|
created_at: Set(chrono::Utc::now().to_rfc3339()),
|
||||||
|
updated_at: Set(chrono::Utc::now().to_rfc3339()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct UpdateTransactionRequest {
|
||||||
|
pub account_id: Option<i64>,
|
||||||
|
pub amount: Option<String>,
|
||||||
|
pub transaction_type: Option<TransactionType>,
|
||||||
|
pub currency_code: Option<String>,
|
||||||
|
pub exchange_rate: Option<Option<String>>,
|
||||||
|
pub transaction_date: Option<String>,
|
||||||
|
pub metadata: Option<Option<TransactionMetadata>>,
|
||||||
|
pub from_account_id: Option<Option<i64>>,
|
||||||
|
pub category_id: Option<Option<i64>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdateTransactionRequest {
|
||||||
|
pub fn apply_to(self, transaction: &mut TransactionActiveModel) {
|
||||||
|
if let Some(account_id) = self.account_id {
|
||||||
|
transaction.account_id = Set(account_id);
|
||||||
|
}
|
||||||
|
if let Some(amount) = self.amount {
|
||||||
|
transaction.amount = Set(amount);
|
||||||
|
}
|
||||||
|
if let Some(transaction_type) = self.transaction_type {
|
||||||
|
transaction.transaction_type = Set(transaction_type.to_string());
|
||||||
|
}
|
||||||
|
if let Some(currency_code) = self.currency_code {
|
||||||
|
transaction.currency_code = Set(currency_code);
|
||||||
|
}
|
||||||
|
if let Some(exchange_rate) = self.exchange_rate {
|
||||||
|
transaction.exchange_rate = Set(exchange_rate);
|
||||||
|
}
|
||||||
|
if let Some(transaction_date) = self.transaction_date {
|
||||||
|
transaction.transaction_date = Set(transaction_date);
|
||||||
|
}
|
||||||
|
if let Some(metadata) = self.metadata {
|
||||||
|
transaction.metadata =
|
||||||
|
Set(metadata.map(|m| serde_json::to_string(&m).unwrap_or_default()));
|
||||||
|
}
|
||||||
|
if let Some(from_account_id) = self.from_account_id {
|
||||||
|
transaction.from_account_id = Set(from_account_id);
|
||||||
|
}
|
||||||
|
if let Some(category_id) = self.category_id {
|
||||||
|
transaction.category_id = Set(category_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn validate<T, U>(
|
||||||
|
&self,
|
||||||
|
account_service: Arc<T>,
|
||||||
|
category_service: Arc<U>,
|
||||||
|
) -> Result<(), TransactionServiceError>
|
||||||
|
where
|
||||||
|
T: crate::services::accounts::AccountsService + ?Sized,
|
||||||
|
U: crate::services::CategoryService + ?Sized,
|
||||||
|
{
|
||||||
|
if let Some(amount) = self.amount.as_ref()
|
||||||
|
&& amount.trim().is_empty()
|
||||||
|
{
|
||||||
|
return Err(TransactionServiceError::Validation(
|
||||||
|
"Amount cannot be empty".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// check if amount is a valid decimal
|
||||||
|
if let Some(amount) = self.amount.as_ref()
|
||||||
|
&& amount.parse::<rust_decimal::Decimal>().is_err()
|
||||||
|
{
|
||||||
|
return Err(TransactionServiceError::Validation(
|
||||||
|
"Amount must be a valid decimal".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(currency_code) = self.currency_code.as_ref()
|
||||||
|
&& currency_code.trim().is_empty()
|
||||||
|
{
|
||||||
|
return Err(TransactionServiceError::Validation(
|
||||||
|
"Currency code cannot be empty".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(transaction_date) = self.transaction_date.as_ref()
|
||||||
|
&& transaction_date.trim().is_empty()
|
||||||
|
{
|
||||||
|
return Err(TransactionServiceError::Validation(
|
||||||
|
"Transaction date cannot be empty".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// check if transaction_date is a valid date
|
||||||
|
if let Some(transaction_date) = self.transaction_date.as_ref()
|
||||||
|
&& chrono::DateTime::parse_from_rfc3339(transaction_date).is_err()
|
||||||
|
{
|
||||||
|
return Err(TransactionServiceError::Validation(
|
||||||
|
"Transaction date must be a valid RFC3339 date".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(Some(metadata)) = self.metadata.as_ref() {
|
||||||
|
metadata.validate()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(account_id) = self.account_id {
|
||||||
|
validate_account_exists(account_id, account_service).await?;
|
||||||
|
}
|
||||||
|
if let Some(Some(category_id)) = self.category_id {
|
||||||
|
validate_category_exists(category_id, category_service).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src-tauri/src/services/transaction/mod.rs
Normal file
9
src-tauri/src/services/transaction/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
pub mod category;
|
||||||
|
mod dto;
|
||||||
|
mod repo;
|
||||||
|
mod service;
|
||||||
|
pub mod tag;
|
||||||
|
|
||||||
|
// re-export as a single module
|
||||||
|
pub use dto::*;
|
||||||
|
pub use service::*;
|
||||||
199
src-tauri/src/services/transaction/repo.rs
Normal file
199
src-tauri/src/services/transaction/repo.rs
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
db::{
|
||||||
|
Db,
|
||||||
|
entities::transaction::{
|
||||||
|
ActiveModel as TransactionActiveModel, Column::AccountId, Entity as TransactionEntity,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
services::transaction::{
|
||||||
|
CreateTransactionRequest, Transaction, TransactionResult, TransactionServiceError,
|
||||||
|
UpdateTransactionRequest, category::repo::CategoryRepoServiceImpl,
|
||||||
|
tag::repo::TagRepoServiceImpl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use sea_orm::{
|
||||||
|
ActiveModelTrait, ActiveValue::Set, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter,
|
||||||
|
TransactionTrait,
|
||||||
|
};
|
||||||
|
|
||||||
|
type Result<T> = TransactionResult<T>;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait TransactionRepoService: Send + Sync + 'static {
|
||||||
|
async fn get_transaction(&self, id: i64) -> Result<Option<Transaction>>;
|
||||||
|
// TODO: pagination
|
||||||
|
// TODO: filter
|
||||||
|
// TODO: sort
|
||||||
|
async fn get_transactions_by_account(&self, account_id: i64) -> Result<Vec<Transaction>>;
|
||||||
|
async fn create_transaction(&self, request: CreateTransactionRequest) -> Result<i64>;
|
||||||
|
async fn create_transaction_with_tx(
|
||||||
|
&self,
|
||||||
|
request: CreateTransactionRequest,
|
||||||
|
tx: &Db,
|
||||||
|
) -> Result<i64>;
|
||||||
|
async fn update_transaction(&self, id: i64, request: UpdateTransactionRequest) -> Result<()>;
|
||||||
|
async fn update_transaction_with_tx(
|
||||||
|
&self,
|
||||||
|
id: i64,
|
||||||
|
request: UpdateTransactionRequest,
|
||||||
|
tx: &Db,
|
||||||
|
) -> Result<()>;
|
||||||
|
async fn delete_transaction(&self, id: i64) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TransactionRepoServiceImpl<T, U, K>
|
||||||
|
where
|
||||||
|
T: super::super::accounts::AccountsService + ?Sized,
|
||||||
|
U: super::category::CategoryService + ?Sized,
|
||||||
|
K: super::tag::TagService + ?Sized,
|
||||||
|
{
|
||||||
|
db: DatabaseConnection,
|
||||||
|
account_service: Arc<T>,
|
||||||
|
category_service: Arc<U>,
|
||||||
|
tag_service: Arc<K>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, U, K> TransactionRepoServiceImpl<T, U, K>
|
||||||
|
where
|
||||||
|
T: super::super::accounts::AccountsService + ?Sized,
|
||||||
|
U: super::category::CategoryService + ?Sized,
|
||||||
|
K: super::tag::TagService + ?Sized,
|
||||||
|
{
|
||||||
|
pub fn new(
|
||||||
|
db: DatabaseConnection,
|
||||||
|
account_service: Arc<T>,
|
||||||
|
category_service: Arc<U>,
|
||||||
|
tag_service: Arc<K>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
db: db.clone(),
|
||||||
|
account_service,
|
||||||
|
tag_service,
|
||||||
|
category_service,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl<T, U, K> TransactionRepoService for TransactionRepoServiceImpl<T, U, K>
|
||||||
|
where
|
||||||
|
T: super::super::accounts::AccountsService + ?Sized,
|
||||||
|
U: super::category::CategoryService + ?Sized,
|
||||||
|
K: super::tag::TagService + ?Sized,
|
||||||
|
{
|
||||||
|
async fn get_transaction(&self, id: i64) -> Result<Option<Transaction>> {
|
||||||
|
let transaction = TransactionEntity::find_by_id(id)
|
||||||
|
.one(&self.db)
|
||||||
|
.await?
|
||||||
|
.map(|model| model.into());
|
||||||
|
Ok(transaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_transactions_by_account(&self, account_id: i64) -> Result<Vec<Transaction>> {
|
||||||
|
let transactions = TransactionEntity::find()
|
||||||
|
.filter(AccountId.eq(account_id))
|
||||||
|
.all(&self.db)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|model| model.into())
|
||||||
|
.collect::<Vec<Transaction>>();
|
||||||
|
Ok(transactions)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_transaction(&self, request: CreateTransactionRequest) -> Result<i64> {
|
||||||
|
self.create_transaction_with_tx(request, &(&self.db).into())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_transaction_with_tx(
|
||||||
|
&self,
|
||||||
|
request: CreateTransactionRequest,
|
||||||
|
outer_tx: &Db,
|
||||||
|
) -> Result<i64> {
|
||||||
|
let inner_tx = match outer_tx {
|
||||||
|
Db::Connection(conn) => Some(conn.begin().await?),
|
||||||
|
Db::Transaction(_) => None,
|
||||||
|
};
|
||||||
|
let tx = inner_tx.as_ref().map_or_else(
|
||||||
|
|| match outer_tx {
|
||||||
|
Db::Connection(conn) => Db::Connection(conn),
|
||||||
|
Db::Transaction(tx) => Db::Transaction(tx),
|
||||||
|
},
|
||||||
|
Db::Transaction,
|
||||||
|
);
|
||||||
|
|
||||||
|
request
|
||||||
|
.validate(self.account_service.clone(), self.category_service.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let new_transaction: TransactionActiveModel = request.into();
|
||||||
|
let res = new_transaction.insert(&tx).await?;
|
||||||
|
|
||||||
|
if let Some(inner_tx) = inner_tx {
|
||||||
|
inner_tx.commit().await?;
|
||||||
|
}
|
||||||
|
Ok(res.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_transaction(&self, id: i64, request: UpdateTransactionRequest) -> Result<()> {
|
||||||
|
self.update_transaction_with_tx(id, request, &(&self.db).into())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_transaction_with_tx(
|
||||||
|
&self,
|
||||||
|
id: i64,
|
||||||
|
request: UpdateTransactionRequest,
|
||||||
|
outer_tx: &Db,
|
||||||
|
) -> Result<()> {
|
||||||
|
let inner_tx = match outer_tx {
|
||||||
|
Db::Connection(conn) => Some(conn.begin().await?),
|
||||||
|
Db::Transaction(_) => None,
|
||||||
|
};
|
||||||
|
let tx = inner_tx.as_ref().map_or_else(
|
||||||
|
|| match outer_tx {
|
||||||
|
Db::Connection(conn) => Db::Connection(conn),
|
||||||
|
Db::Transaction(tx) => Db::Transaction(tx),
|
||||||
|
},
|
||||||
|
Db::Transaction,
|
||||||
|
);
|
||||||
|
//
|
||||||
|
request
|
||||||
|
.validate(self.account_service.clone(), self.category_service.clone())
|
||||||
|
.await?;
|
||||||
|
let transaction = TransactionEntity::find_by_id(id)
|
||||||
|
.one(&tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
TransactionServiceError::TargetNotFound(format!(
|
||||||
|
"Transaction with id {} not found",
|
||||||
|
id
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let mut transaction: TransactionActiveModel = transaction.into();
|
||||||
|
request.apply_to(&mut transaction);
|
||||||
|
transaction.updated_at = Set(chrono::Utc::now().to_rfc3339());
|
||||||
|
|
||||||
|
transaction.update(&tx).await?;
|
||||||
|
//
|
||||||
|
if let Some(inner_tx) = inner_tx {
|
||||||
|
inner_tx.commit().await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_transaction(&self, id: i64) -> Result<()> {
|
||||||
|
// delete the tag-transaction relations
|
||||||
|
let tx = self.db.begin().await?;
|
||||||
|
self.tag_service
|
||||||
|
.update_tags_for_transaction_with_tx(id, vec![], &(&tx).into())
|
||||||
|
.await?;
|
||||||
|
// delete the transaction itself
|
||||||
|
TransactionEntity::delete_by_id(id).exec(&tx).await?;
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
115
src-tauri/src/services/transaction/service.rs
Normal file
115
src-tauri/src/services/transaction/service.rs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use sea_orm::DatabaseConnection;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
db::Db,
|
||||||
|
services::transaction::{
|
||||||
|
CreateTransactionRequest, Transaction, TransactionServiceError, UpdateTransactionRequest,
|
||||||
|
repo::TransactionRepoService,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub type TransactionResult<T> = std::result::Result<T, TransactionServiceError>;
|
||||||
|
type Result<T> = TransactionResult<T>;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait TransactionService: Send + Sync + 'static {
|
||||||
|
async fn get_transaction(&self, id: i64) -> Result<Option<Transaction>>;
|
||||||
|
// TODO: pagination
|
||||||
|
// TODO: filter
|
||||||
|
// TODO: sort
|
||||||
|
async fn get_transactions_by_account(&self, account_id: i64) -> Result<Vec<Transaction>>;
|
||||||
|
async fn create_transaction(&self, request: CreateTransactionRequest) -> Result<i64>;
|
||||||
|
async fn create_transaction_with_tx(
|
||||||
|
&self,
|
||||||
|
request: CreateTransactionRequest,
|
||||||
|
tx: &Db,
|
||||||
|
) -> Result<i64>;
|
||||||
|
async fn update_transaction(&self, id: i64, request: UpdateTransactionRequest) -> Result<()>;
|
||||||
|
async fn update_transaction_with_tx(
|
||||||
|
&self,
|
||||||
|
id: i64,
|
||||||
|
request: UpdateTransactionRequest,
|
||||||
|
tx: &Db,
|
||||||
|
) -> Result<()>;
|
||||||
|
async fn delete_transaction(&self, id: i64) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TransactionServiceImpl<T>
|
||||||
|
where
|
||||||
|
T: super::repo::TransactionRepoService + ?Sized,
|
||||||
|
{
|
||||||
|
db: DatabaseConnection,
|
||||||
|
repo: Arc<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, U, K> TransactionServiceImpl<super::repo::TransactionRepoServiceImpl<T, U, K>>
|
||||||
|
where
|
||||||
|
T: super::super::accounts::AccountsService + ?Sized,
|
||||||
|
U: super::category::CategoryService + ?Sized,
|
||||||
|
K: super::tag::TagService + ?Sized,
|
||||||
|
{
|
||||||
|
pub fn new(
|
||||||
|
db: DatabaseConnection,
|
||||||
|
account_service: Arc<T>,
|
||||||
|
category_service: Arc<U>,
|
||||||
|
tag_service: Arc<K>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
db: db.clone(),
|
||||||
|
repo: Arc::new(super::repo::TransactionRepoServiceImpl::new(
|
||||||
|
db,
|
||||||
|
account_service,
|
||||||
|
category_service,
|
||||||
|
tag_service,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl<T, U, K> TransactionService
|
||||||
|
for TransactionServiceImpl<super::repo::TransactionRepoServiceImpl<T, U, K>>
|
||||||
|
where
|
||||||
|
T: super::super::accounts::AccountsService + ?Sized,
|
||||||
|
U: super::category::CategoryService + ?Sized,
|
||||||
|
K: super::tag::TagService + ?Sized,
|
||||||
|
{
|
||||||
|
async fn get_transaction(&self, id: i64) -> Result<Option<Transaction>> {
|
||||||
|
self.repo.get_transaction(id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_transactions_by_account(&self, account_id: i64) -> Result<Vec<Transaction>> {
|
||||||
|
self.repo.get_transactions_by_account(account_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_transaction(&self, request: CreateTransactionRequest) -> Result<i64> {
|
||||||
|
self.repo.create_transaction(request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_transaction_with_tx(
|
||||||
|
&self,
|
||||||
|
request: CreateTransactionRequest,
|
||||||
|
tx: &Db,
|
||||||
|
) -> Result<i64> {
|
||||||
|
self.repo.create_transaction_with_tx(request, tx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_transaction(&self, id: i64, request: UpdateTransactionRequest) -> Result<()> {
|
||||||
|
self.repo.update_transaction(id, request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_transaction_with_tx(
|
||||||
|
&self,
|
||||||
|
id: i64,
|
||||||
|
request: UpdateTransactionRequest,
|
||||||
|
tx: &Db,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.repo.update_transaction_with_tx(id, request, tx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_transaction(&self, id: i64) -> Result<()> {
|
||||||
|
self.repo.delete_transaction(id).await
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user