Skip to content

Commit

Permalink
feat(paypal): send invoice via email
Browse files Browse the repository at this point in the history
  • Loading branch information
Defelo committed Nov 10, 2024
1 parent 686a376 commit a25eee4
Show file tree
Hide file tree
Showing 44 changed files with 2,864 additions and 178 deletions.
493 changes: 461 additions & 32 deletions Cargo.lock

Large diffs are not rendered by default.

1,620 changes: 1,518 additions & 102 deletions Cargo.nix

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ members = [
"academy_extern/*",
"academy_models",
"academy_persistence/*",
"academy_render/*",
"academy_shared/*",
"academy_templates/contracts",
"academy_templates/impl",
Expand All @@ -35,7 +36,6 @@ unsafe_code = "forbid"

[workspace.lints.clippy]
needless_return = "allow"
allow_attributes_without_reason = "warn"
clone_on_ref_ptr = "warn"
dbg_macro = "warn"
renamed_function_params = "warn"
Expand Down Expand Up @@ -80,6 +80,8 @@ academy_extern_impl.path = "academy_extern/impl"
academy_models.path = "academy_models"
academy_persistence_contracts.path = "academy_persistence/contracts"
academy_persistence_postgres.path = "academy_persistence/postgres"
academy_render_contracts.path = "academy_render/contracts"
academy_render_impl.path = "academy_render/impl"
academy_shared_contracts.path = "academy_shared/contracts"
academy_shared_impl.path = "academy_shared/impl"
academy_templates_contracts.path = "academy_templates/contracts"
Expand All @@ -105,18 +107,20 @@ nutype = { version = "0.5.0", default-features = false, features = ["std", "rege
oauth2 = { version = "4.4.2", default-features = false, features = ["reqwest", "rustls-tls"] }
paste = { version = "1.0.15", default-features = false }
pretty_assertions = { version = "1.4.1", default-features = false, features = ["std"] }
proc-macro2 = { version = "1.0.89", default-features = false, features = ["proc-macro"] }
quote = { version = "1.0.37", default-features = false, features = ["proc-macro"] }
rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] }
regex = { version = "1.11.1", default-features = false }
reqwest = { version = "0.12.9", default-features = false, features = ["http2", "rustls-tls", "json"] }
rust_decimal = { version = "1.36.0", default-features = false, features = ["std", "serde-str"] }
rust_decimal_macros = { version = "1.36.0", default-features = false }
schemars = { version = "0.8.21", default-features = false, features = ["derive", "preserve_order", "uuid1", "url"] }
serde = { version = "1.0.214", default-features = false, features = ["derive", "std"] }
serde_json = { version = "1.0.132", default-features = false, features = ["std"] }
sha2 = { version = "0.10.8", default-features = false }
syn = { version = "2.0.87", default-features = false, features = ["parsing", "proc-macro", "derive", "printing"] }
proc-macro2 = { version = "1.0.89", default-features = false, features = ["proc-macro"] }
thiserror = { version = "2.0.2", default-features = false }
tokio = { version = "1.41.1", default-features = false, features = ["rt-multi-thread", "macros", "sync"] }
tokio = { version = "1.41.1", default-features = false, features = ["rt-multi-thread", "macros", "sync", "fs", "process"] }
tracing = { version = "0.1.40", default-features = false, features = ["attributes"] }
tracing-subscriber = { version = "0.3.18", default-features = false, features = ["ansi", "fmt", "env-filter"] }
url = { version = "2.5.3", default-features = false, features = ["serde"] }
Expand Down
2 changes: 2 additions & 0 deletions academy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ academy_extern_impl.workspace = true
academy_models.workspace = true
academy_persistence_contracts.workspace = true
academy_persistence_postgres.workspace = true
academy_render_contracts.workspace = true
academy_render_impl.workspace = true
academy_shared_contracts.workspace = true
academy_shared_impl.workspace = true
academy_templates_impl.workspace = true
Expand Down
1 change: 1 addition & 0 deletions academy/src/commands/email.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ async fn test(config: Config, recipient: EmailAddressWithName) -> anyhow::Result
body: "Email deliverability seems to be working!".into(),
content_type: ContentType::Text,
reply_to: None,
attachments: Vec::new(),
})
.await
.and_then(|r| {
Expand Down
16 changes: 16 additions & 0 deletions academy/src/environment/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use academy_extern_impl::{
paypal::PaypalApiServiceConfig, recaptcha::RecaptchaApiServiceConfig, vat::VatApiServiceConfig,
};
use academy_models::oauth2::OAuth2Provider;
use academy_render_impl::pdf::RenderPdfServiceConfig;
use academy_shared_impl::{
captcha::{CaptchaServiceConfig, RecaptchaCaptchaServiceConfig},
jwt::JwtServiceConfig,
Expand All @@ -38,6 +39,9 @@ provider! {
VatApiServiceConfig,
PaypalApiServiceConfig,

// Render
RenderPdfServiceConfig,

// Shared
CaptchaServiceConfig,
JwtServiceConfig,
Expand Down Expand Up @@ -80,6 +84,9 @@ provider! {
vat_api_service_config: VatApiServiceConfig,
paypal_api_service_config: PaypalApiServiceConfig,

// Render
render_pdf_service_config: RenderPdfServiceConfig,

// Shared
captcha_service_config: CaptchaServiceConfig,
jwt_service_config: JwtServiceConfig,
Expand Down Expand Up @@ -129,6 +136,11 @@ impl ConfigProvider {
config.paypal.client_secret.clone(),
);

// Render
let render_pdf_service_config = RenderPdfServiceConfig {
chrome_bin: config.render.chrome_bin.clone().into(),
};

// Shared
let captcha_service_config = match config.recaptcha.as_ref() {
Some(recaptcha) => CaptchaServiceConfig::Recaptcha(RecaptchaCaptchaServiceConfig {
Expand Down Expand Up @@ -214,6 +226,7 @@ impl ConfigProvider {

let paypal_feature_config = PaypalFeatureConfig {
purchase_range: config.coin.purchase_min..=config.coin.purchase_max,
vat_percent: config.finance.vat_percent,
};

Ok(Self {
Expand All @@ -227,6 +240,9 @@ impl ConfigProvider {
vat_api_service_config,
paypal_api_service_config,

// Render
render_pdf_service_config,

// Shared
jwt_service_config,
totp_service_config,
Expand Down
18 changes: 16 additions & 2 deletions academy/src/environment/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ use academy_persistence_postgres::{
paypal::PostgresPaypalRepository, session::PostgresSessionRepository,
user::PostgresUserRepository, PostgresDatabase,
};
use academy_render_impl::pdf::RenderPdfServiceImpl;
use academy_shared_impl::{
captcha::CaptchaServiceImpl, hash::HashServiceImpl, id::IdServiceImpl, jwt::JwtServiceImpl,
password::PasswordServiceImpl, secret::SecretServiceImpl, time::TimeServiceImpl,
Expand Down Expand Up @@ -77,6 +78,9 @@ pub type PaypalApi = PaypalApiServiceImpl;
// Template
pub type Template = TemplateServiceImpl;

// Render
pub type RenderPdf = RenderPdfServiceImpl;

// Shared
pub type Captcha = CaptchaServiceImpl<RecaptchaApi>;
pub type Hash = HashServiceImpl;
Expand Down Expand Up @@ -172,8 +176,18 @@ pub type OAuth2Registration = OAuth2RegistrationServiceImpl<Secret, Cache>;
pub type CoinFeature = CoinFeatureServiceImpl<Database, Auth, UserRepo, CoinRepo, Coin>;
pub type Coin = CoinServiceImpl<CoinRepo>;

pub type PaypalFeature =
PaypalFeatureServiceImpl<Database, Auth, PaypalApi, UserRepo, PaypalRepo, PaypalCoinOrder>;
pub type PaypalFeature = PaypalFeatureServiceImpl<
Database,
Auth,
Time,
PaypalApi,
UserRepo,
PaypalRepo,
PaypalCoinOrder,
Template,
TemplateEmail,
RenderPdf,
>;
pub type PaypalCoinOrder = PaypalCoinOrderServiceImpl<Time, PaypalRepo, CoinRepo>;

pub type Internal = InternalServiceImpl<Database, AuthInternal, UserRepo, Coin>;
Binary file not shown.
Binary file added academy_assets/assets/email/logo-text.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
182 changes: 182 additions & 0 deletions academy_assets/assets/templates/invoice.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
<html>
<head>
<title>{{ title }}</title>
<style>
body {
width: 210mm;
}

.content {
padding: 5%;
font-family: Helvetica;
display: flex;
flex-direction: column;
gap: 32px;
}

.row {
display: flex;
justify-content: space-between;
}

.right {
justify-content: right;
}

.logo {
width: 250px;
}

.title {
font-size: 40;
margin: 0;
}

.academy-details {
display: flex;
flex-direction: column;
gap: 2px;
align-items: end;
}

.academy-details a {
color: unset;
text-decoration: none;
}

.customer-details {
display: flex;
flex-direction: column;
gap: 2px;
}

.details {
display: flex;
flex-direction: column;
gap: 2px;
}

.details > .row {
gap: 24px;
}

.table {
display: grid;
grid-template-columns: 3fr 1fr 1fr 1fr;
}

.table-row {
grid-column: 1 / 5;
display: grid;
grid-template-columns: subgrid;
padding: 4px;
}

.table-row > * {
padding: 0 4px;
}

.table-header {
background: #0b5156;
color: white;
}

.table-row:not(.table-header):nth-child(odd) {
background: #dddddd;
}

.table-row:not(.table-header):nth-child(even) {
background: #ffffff;
}

.table-right {
justify-self: right;
}

.summary {
width: 40%;
}
</style>
</head>

<body>
<div class="content">
<div class="row right">
<img class="logo" src="data:image/png;base64,{{ logo_base64 }}" />
</div>

<div class="row">
<p class="title">{{ title }}</p>
<div class="academy-details">
<div><b>bootstrap academy GmbH</b></div>
<div>Wittelsbacherplatz 1</div>
<div>80333 München</div>
<div><a href="tel:+4989248862510">+49 89 24 88 62 51 0</a></div>
<div>
<a href="mailto:[email protected]">[email protected]</a>
</div>
<div>USt.-IdNr.: DE354823768</div>
<div>Handelsregister: HRB 275681</div>
</div>
</div>

<div class="row">
<div class="customer-details">
{% for line in customer_details -%}
<div>{{ line }}</div>
{%- endfor %}
</div>

<div class="details">
<div class="row">
<b>Datum</b>
<div>{{ timestamp | date(format="%d.%m.%Y") }}</div>
</div>
<div class="row">
<b>Rechnungs-Nr.</b>
<div>{{ invoice_number }}</div>
</div>
<div class="row">
<b>Gesamtbetrag</b>
<div>{{ gross_total }} EUR</div>
</div>
</div>
</div>

<div class="table">
<div class="table-row table-header">
<div>Bezeichnung</div>
<div class="table-right">Einzelpreis</div>
<div class="table-right">Menge</div>
<div class="table-right">Gesamtpreis</div>
</div>

{% for item in items -%}
<div class="table-row">
<div>{{ item.description }}</div>
<div class="table-right">{{ item.net_unit }} EUR</div>
<div class="table-right">{{ item.count }}</div>
<div class="table-right">{{ item.net_total }} EUR</div>
</div>
{%- endfor %}
</div>

<div class="row right">
<div class="details summary">
<div class="row">
<b>Nettobetrag</b>
<div>{{ net_total }} EUR</div>
</div>
<div class="row">
<b>zzgl. {{ vat_percent }}% MwSt.</b>
<div>{{ vat_total }} EUR</div>
</div>
<div class="row">
<b>Gesamtbetrag</b>
<div>{{ gross_total }} EUR</div>
</div>
</div>
</div>
</div>
</body>
</html>
12 changes: 12 additions & 0 deletions academy_assets/assets/templates/purchase_confirmation.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{% extends "base" %}
{% block title %}Kaufbestätigung{% endblock title %}
{% block content %}
<p>
Du hast erfolgreich {{ coins }} MorphCoins gekauft! Das entspricht {{ gross_total }}€ inklusive {{ vat_percent }}% MwSt. von {{ vat_total }}€.
Wir hoffen, du kannst sie gut nutzen!
</p>

<p>
Viel Spaß beim Lernen.
</p>
{% endblock content %}
1 change: 1 addition & 0 deletions academy_config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ academy_models.workspace = true
anyhow.workspace = true
config = { version = "0.14.1", default-features = false, features = ["toml"] }
regex.workspace = true
rust_decimal.workspace = true
serde.workspace = true

[dev-dependencies]
Expand Down
Loading

0 comments on commit a25eee4

Please sign in to comment.