Skip to content

Commit

Permalink
feat(cli): add command to generate missing invoices
Browse files Browse the repository at this point in the history
  • Loading branch information
Defelo committed Dec 4, 2024
1 parent 182de49 commit b614d5b
Show file tree
Hide file tree
Showing 12 changed files with 143 additions and 5 deletions.
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions Cargo.nix
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,10 @@ rec {
name = "academy_extern_impl";
packageId = "academy_extern_impl";
}
{
name = "academy_finance_contracts";
packageId = "academy_finance_contracts";
}
{
name = "academy_finance_impl";
packageId = "academy_finance_impl";
Expand Down Expand Up @@ -728,6 +732,12 @@ rec {
packageId = "clap_complete";
usesDefaultFeatures = false;
}
{
name = "futures";
packageId = "futures";
usesDefaultFeatures = false;
features = [ "std" ];
}
{
name = "hex";
packageId = "hex";
Expand Down Expand Up @@ -3013,6 +3023,12 @@ rec {
usesDefaultFeatures = false;
features = [ "serde" "clock" ];
}
{
name = "futures";
packageId = "futures";
usesDefaultFeatures = false;
features = [ "std" ];
}
{
name = "mockall";
packageId = "mockall";
Expand Down Expand Up @@ -3075,6 +3091,12 @@ rec {
usesDefaultFeatures = false;
features = [ "serde" "clock" ];
}
{
name = "futures";
packageId = "futures";
usesDefaultFeatures = false;
features = [ "std" ];
}
{
name = "ouroboros";
packageId = "ouroboros";
Expand Down
2 changes: 2 additions & 0 deletions academy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ academy_di.workspace = true
academy_email_contracts.workspace = true
academy_email_impl.workspace = true
academy_extern_impl.workspace = true
academy_finance_contracts.workspace = true
academy_finance_impl.workspace = true
academy_models.workspace = true
academy_persistence_contracts.workspace = true
Expand All @@ -46,6 +47,7 @@ anyhow.workspace = true
chrono.workspace = true
clap.workspace = true
clap_complete.workspace = true
futures.workspace = true
sentry = { version = "0.35.0", default-features = false, features = ["anyhow", "backtrace", "contexts", "panic", "debug-images", "reqwest", "rustls", "tracing"] }
serde_json.workspace = true
tokio.workspace = true
Expand Down
54 changes: 54 additions & 0 deletions academy/src/commands/admin/invoice.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use academy_config::Config;
use academy_di::Provide;
use academy_finance_contracts::FinanceService;
use academy_persistence_contracts::{paypal::PaypalRepository, Database};
use clap::Subcommand;
use futures::TryStreamExt;
use indicatif::ProgressBar;

use crate::{
cache, database, email,
environment::{types, ConfigProvider, Provider},
};

#[derive(Debug, Subcommand)]
pub enum AdminInvoiceCommand {
/// Generate missing invoice pdf files
#[command(aliases(["g"]))]
Generate,
}

impl AdminInvoiceCommand {
pub async fn invoke(self, config: Config) -> anyhow::Result<()> {
match self {
AdminInvoiceCommand::Generate => generate(config).await,
}
}
}

async fn generate(config: Config) -> anyhow::Result<()> {
let database = database::connect(&config.database).await?;
let cache = cache::connect(&config.cache).await?;
let email_service = email::connect(&config.email).await?;
let config_provider = ConfigProvider::new(&config)?;
let mut provider = Provider::new(config_provider, database, cache, email_service);

let db: types::Database = provider.provide();
let mut txn = db.begin_transaction().await?;

let finance_service: types::Finance = provider.provide();
let paypal_repo: types::PaypalRepo = provider.provide();

let cnt = paypal_repo.count_coin_orders(&mut txn).await?;
let bar = ProgressBar::new(cnt);
let mut stream = std::pin::pin!(paypal_repo.stream_coin_orders(&mut txn));
let mut txn = db.begin_transaction().await?;
while let Some(coin_order) = stream.try_next().await? {
finance_service
.get_invoice_pdf(&mut txn, coin_order.invoice_number)
.await?;
bar.inc(1);
}

Ok(())
}
11 changes: 10 additions & 1 deletion academy/src/commands/admin/mod.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
use academy_config::Config;
use clap::Subcommand;
use invoice::AdminInvoiceCommand;
use user::AdminUserCommand;

mod invoice;
mod user;

#[derive(Debug, Subcommand)]
pub enum AdminCommand {
/// Manager user accounts
/// Manage user accounts
#[command(aliases(["u"]))]
User {
#[command(subcommand)]
command: AdminUserCommand,
},
/// Manage invoices
#[command(aliases(["i"]))]
Invoice {
#[command(subcommand)]
command: AdminInvoiceCommand,
},
}

impl AdminCommand {
pub async fn invoke(self, config: Config) -> anyhow::Result<()> {
match self {
AdminCommand::User { command } => command.invoke(config).await,
AdminCommand::Invoice { command } => command.invoke(config).await,
}
}
}
1 change: 1 addition & 0 deletions academy_persistence/contracts/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ mock = ["dep:mockall"]
academy_models.workspace = true
anyhow.workspace = true
chrono.workspace = true
futures.workspace = true
mockall = { workspace = true, optional = true }
thiserror.workspace = true
10 changes: 10 additions & 0 deletions academy_persistence/contracts/src/paypal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::future::Future;

use academy_models::paypal::{PaypalCoinOrder, PaypalOrderId};
use chrono::{DateTime, Utc};
use futures::Stream;

#[cfg_attr(feature = "mock", mockall::automock)]
pub trait PaypalRepository<Txn: Send + Sync + 'static>: Send + Sync + 'static {
Expand All @@ -12,6 +13,15 @@ pub trait PaypalRepository<Txn: Send + Sync + 'static>: Send + Sync + 'static {
order: &PaypalCoinOrder,
) -> impl Future<Output = anyhow::Result<()>> + Send;

/// Return the number of coin orders.
fn count_coin_orders(&self, txn: &mut Txn) -> impl Future<Output = anyhow::Result<u64>> + Send;

/// Return a stream of all coin orders.
fn stream_coin_orders(
&self,
txn: &mut Txn,
) -> impl Stream<Item = anyhow::Result<PaypalCoinOrder>>;

/// Return the coin order with the given id.
fn get_coin_order(
&self,
Expand Down
1 change: 1 addition & 0 deletions academy_persistence/postgres/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ anyhow.workspace = true
bb8 = { version = "0.8.6", default-features = false }
bb8-postgres = { version = "0.8.1", default-features = false, features = ["with-chrono-0_4", "with-uuid-1"] }
chrono.workspace = true
futures.workspace = true
ouroboros = { version = "0.18.4", default-features = false }
paste.workspace = true
tracing.workspace = true
Expand Down
27 changes: 27 additions & 0 deletions academy_persistence/postgres/src/paypal.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use std::sync::LazyLock;

use academy_di::Build;
use academy_models::paypal::{PaypalCoinOrder, PaypalOrderId};
use academy_persistence_contracts::paypal::PaypalRepository;
use bb8_postgres::tokio_postgres::Row;
use chrono::{DateTime, Utc};
use futures::{Stream, StreamExt, TryFutureExt};
use uuid::Uuid;

use crate::{arg_indices, columns, ColumnCounter, PostgresTransaction};
Expand Down Expand Up @@ -39,6 +42,30 @@ impl PaypalRepository<PostgresTransaction> for PostgresPaypalRepository {
.map_err(Into::into)
}

async fn count_coin_orders(&self, txn: &mut PostgresTransaction) -> anyhow::Result<u64> {
txn.txn()
.query_one("select count(*) from paypal_coin_orders", &[])
.await
.map(|row| row.get::<_, i64>(0) as _)
.map_err(Into::into)
}

fn stream_coin_orders(
&self,
txn: &mut PostgresTransaction,
) -> impl Stream<Item = anyhow::Result<PaypalCoinOrder>> {
static STMT: LazyLock<String> = LazyLock::new(|| {
format!("select {PAYPAL_COIN_ORDER_COLS} from paypal_coin_orders pco")
});
txn.txn()
.query_raw(&*STMT, std::iter::empty::<&str>())
.try_flatten_stream()
.map(|row| {
row.map_err(anyhow::Error::from)
.and_then(|row| decode_paypal_coin_order(&row, &mut Default::default()))
})
}

async fn get_coin_order(
&self,
txn: &mut PostgresTransaction,
Expand Down
8 changes: 8 additions & 0 deletions academy_persistence/postgres/tests/repos/paypal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use academy_demo::user::FOO;
use academy_models::paypal::PaypalCoinOrder;
use academy_persistence_contracts::{paypal::PaypalRepository, Database, Transaction};
use academy_persistence_postgres::paypal::PostgresPaypalRepository;
use futures::StreamExt;

use crate::common::setup;

Expand All @@ -25,6 +26,7 @@ async fn get_create_capture() {
REPO.get_coin_order(&mut txn, &order.id).await.unwrap(),
None
);
assert_eq!(REPO.count_coin_orders(&mut txn).await.unwrap(), 0);

REPO.create_coin_order(&mut txn, &order).await.unwrap();
txn.commit().await.unwrap();
Expand Down Expand Up @@ -52,6 +54,12 @@ async fn get_create_capture() {
.unwrap(),
order
);

assert_eq!(REPO.count_coin_orders(&mut txn).await.unwrap(), 1);
let mut stream = std::pin::pin!(REPO.stream_coin_orders(&mut txn));
let result = stream.next().await.unwrap().unwrap();
assert_eq!(result, order);
assert!(stream.next().await.is_none());
}

#[tokio::test]
Expand Down
4 changes: 2 additions & 2 deletions academy_render/impl/src/pdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ use std::{path::PathBuf, process::Stdio, sync::Arc};

use academy_di::Build;
use academy_render_contracts::pdf::RenderPdfService;
use academy_utils::trace_instrument;
use anyhow::{anyhow, Context};
use tempfile::tempdir;
use tracing::instrument;

#[derive(Debug, Clone, Build)]
pub struct RenderPdfServiceImpl {
Expand All @@ -17,7 +17,7 @@ pub struct RenderPdfServiceConfig {
}

impl RenderPdfService for RenderPdfServiceImpl {
#[trace_instrument(skip(self))]
#[instrument(skip(self, html))]
async fn render(&self, html: &str) -> anyhow::Result<Vec<u8>> {
let dir = tempdir().context("Failed to create tempdir")?;
let index_path = dir.path().join("index.html");
Expand Down
4 changes: 2 additions & 2 deletions academy_templates/impl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ use std::{fmt::Debug, sync::Arc};
use academy_assets::templates;
use academy_di::Build;
use academy_templates_contracts::{Template, TemplateService, TEMPLATES};
use academy_utils::trace_instrument;
use anyhow::Context;
use tera::Tera;
use tracing::instrument;

#[derive(Debug, Clone, Build)]
pub struct TemplateServiceImpl {
Expand All @@ -31,7 +31,7 @@ impl Default for State {
}

impl TemplateService for TemplateServiceImpl {
#[trace_instrument(skip(self))]
#[instrument(skip(self))]
fn render<T: Template>(&self, template: &T) -> anyhow::Result<String> {
let context = tera::Context::from_serialize(template)
.with_context(|| format!("Failed to build tera context for template {}", T::NAME))?;
Expand Down

0 comments on commit b614d5b

Please sign in to comment.