Skip to content

Commit

Permalink
Email Templates and Offers (#528)
Browse files Browse the repository at this point in the history
* change all db pool in `Campaign` to transactions

* move extracting user_id from request to helper function

* add slugs to `Campaign` and `Organisation`

* add endpoints to check slug availability

* slug utility functions and checks

* email templating and offer CRUD

* fix email_template auth service join

* offer CRUD

* ran `cargo fmt` & remove unused imports
  • Loading branch information
KavikaPalletenne authored Nov 29, 2024
1 parent 088a48e commit 2ec5c18
Show file tree
Hide file tree
Showing 26 changed files with 1,119 additions and 272 deletions.
1 change: 1 addition & 0 deletions backend/migrations/20240406024211_create_organisations.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
CREATE TABLE organisations (
id BIGINT PRIMARY KEY,
slug TEXT NOT NULL UNIQUE,
name TEXT NOT NULL UNIQUE,
logo UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
Expand Down
4 changes: 3 additions & 1 deletion backend/migrations/20240406025537_create_campaigns.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
CREATE TABLE campaigns (
id BIGINT PRIMARY KEY,
organisation_id BIGINT NOT NULL,
slug TEXT NOT NULL,
name TEXT NOT NULL,
cover_image UUID,
description TEXT,
Expand All @@ -12,7 +13,8 @@ CREATE TABLE campaigns (
FOREIGN KEY(organisation_id)
REFERENCES organisations(id)
ON DELETE CASCADE
ON UPDATE CASCADE
ON UPDATE CASCADE,
UNIQUE (organisation_id, slug)
);

CREATE TABLE campaign_roles (
Expand Down
12 changes: 12 additions & 0 deletions backend/migrations/20241124054711_email_templates.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE email_templates (
id BIGINT PRIMARY KEY,
organisation_id BIGINT NOT NULL,
name TEXT NOT NULL,
template TEXT NOT NULL,
CONSTRAINT FK_email_templates_organisations
FOREIGN KEY(organisation_id)
REFERENCES organisations(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
UNIQUE (organisation_id, name)
);
32 changes: 32 additions & 0 deletions backend/migrations/20241126113027_offers.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
CREATE TYPE offer_status AS ENUM ('Draft', 'Sent', 'Accepted', 'Declined');

CREATE TABLE offers (
id BIGINT PRIMARY KEY,
campaign_id BIGINT NOT NULL,
application_id BIGINT NOT NULL,
email_template_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
expiry TIMESTAMPTZ NOT NULL,
status offer_status NOT NULL DEFAULT 'Draft',
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT FK_offers_campaigns
FOREIGN KEY(campaign_id)
REFERENCES campaigns(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT FK_offers_applications
FOREIGN KEY(application_id)
REFERENCES applications(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT FK_offers_email_templates
FOREIGN KEY(email_template_id)
REFERENCES email_templates(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT FK_offers_roles
FOREIGN KEY(role_id)
REFERENCES campaign_roles(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
2 changes: 1 addition & 1 deletion backend/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ rust-s3 = "0.34.0"
rs-snowflake = "0.6"
jsonwebtoken = "9.1"
dotenvy = "0.15"

handlebars = "6.2"
10 changes: 5 additions & 5 deletions backend/server/src/handler/answer.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::models::answer::{Answer, NewAnswer};
use crate::models::app::AppState;
use crate::models::auth::{AnswerOwner, ApplicationOwner, AuthUser};
use crate::models::auth::{AnswerOwner, ApplicationOwner};
use crate::models::error::ChaosError;
use crate::models::transaction::DBTransaction;
use axum::extract::{Json, Path, State};
Expand All @@ -13,14 +13,14 @@ pub struct AnswerHandler;
impl AnswerHandler {
pub async fn create(
State(state): State<AppState>,
Path(path): Path<i64>,
user: AuthUser,
Path(application_id): Path<i64>,
_user: ApplicationOwner,
mut transaction: DBTransaction<'_>,
Json(data): Json<NewAnswer>,
) -> Result<impl IntoResponse, ChaosError> {
// TODO: Check whether the question is contained in the campaign being applied to
let id = Answer::create(
user.user_id,
data.application_id,
application_id,
data.question_id,
data.answer_data,
state.snowflake_generator,
Expand Down
80 changes: 67 additions & 13 deletions backend/server/src/handler/campaign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::models::auth::AuthUser;
use crate::models::auth::CampaignAdmin;
use crate::models::campaign::Campaign;
use crate::models::error::ChaosError;
use crate::models::offer::Offer;
use crate::models::role::{Role, RoleUpdate};
use crate::models::transaction::DBTransaction;
use axum::extract::{Json, Path, State};
Expand All @@ -15,67 +16,87 @@ use axum::response::IntoResponse;
pub struct CampaignHandler;
impl CampaignHandler {
pub async fn get(
State(state): State<AppState>,
mut transaction: DBTransaction<'_>,
Path(id): Path<i64>,
_user: AuthUser,
) -> Result<impl IntoResponse, ChaosError> {
let campaign = Campaign::get(id, &state.db).await?;
let campaign = Campaign::get(id, &mut transaction.tx).await?;
transaction.tx.commit().await?;
Ok((StatusCode::OK, Json(campaign)))
}

pub async fn get_by_slugs(
mut transaction: DBTransaction<'_>,
Path((organisation_slug, campaign_slug)): Path<(String, String)>,
_user: AuthUser,
) -> Result<impl IntoResponse, ChaosError> {
let campaign =
Campaign::get_by_slugs(organisation_slug, campaign_slug, &mut transaction.tx).await?;
transaction.tx.commit().await?;
Ok((StatusCode::OK, Json(campaign)))
}

pub async fn get_all(
State(state): State<AppState>,
mut transaction: DBTransaction<'_>,
_user: AuthUser,
) -> Result<impl IntoResponse, ChaosError> {
let campaigns = Campaign::get_all(&state.db).await?;
let campaigns = Campaign::get_all(&mut transaction.tx).await?;
transaction.tx.commit().await?;
Ok((StatusCode::OK, Json(campaigns)))
}

pub async fn update(
State(state): State<AppState>,
mut transaction: DBTransaction<'_>,
Path(id): Path<i64>,
_admin: CampaignAdmin,
Json(request_body): Json<models::campaign::CampaignUpdate>,
) -> Result<impl IntoResponse, ChaosError> {
Campaign::update(id, request_body, &state.db).await?;
Campaign::update(id, request_body, &mut transaction.tx).await?;
transaction.tx.commit().await?;
Ok((StatusCode::OK, "Successfully updated campaign"))
}

pub async fn update_banner(
mut transaction: DBTransaction<'_>,
State(state): State<AppState>,
Path(id): Path<i64>,
_admin: CampaignAdmin,
) -> Result<impl IntoResponse, ChaosError> {
let banner_url = Campaign::update_banner(id, &state.db, &state.storage_bucket).await?;
let banner_url =
Campaign::update_banner(id, &mut transaction.tx, &state.storage_bucket).await?;
transaction.tx.commit().await?;
Ok((StatusCode::OK, Json(banner_url)))
}

pub async fn delete(
State(state): State<AppState>,
mut transaction: DBTransaction<'_>,
Path(id): Path<i64>,
_admin: CampaignAdmin,
) -> Result<impl IntoResponse, ChaosError> {
Campaign::delete(id, &state.db).await?;
Campaign::delete(id, &mut transaction.tx).await?;
transaction.tx.commit().await?;
Ok((StatusCode::OK, "Successfully deleted campaign"))
}

pub async fn create_role(
mut transaction: DBTransaction<'_>,
State(state): State<AppState>,
Path(id): Path<i64>,
_admin: CampaignAdmin,
Json(data): Json<RoleUpdate>,
) -> Result<impl IntoResponse, ChaosError> {
Role::create(id, data, &state.db, state.snowflake_generator).await?;
Role::create(id, data, &mut transaction.tx, state.snowflake_generator).await?;
transaction.tx.commit().await?;
Ok((StatusCode::OK, "Successfully created role"))
}

pub async fn get_roles(
State(state): State<AppState>,
mut transaction: DBTransaction<'_>,
Path(id): Path<i64>,
_user: AuthUser,
) -> Result<impl IntoResponse, ChaosError> {
let roles = Role::get_all_in_campaign(id, &state.db).await?;

let roles = Role::get_all_in_campaign(id, &mut transaction.tx).await?;
transaction.tx.commit().await?;
Ok((StatusCode::OK, Json(roles)))
}

Expand Down Expand Up @@ -107,4 +128,37 @@ impl CampaignHandler {
transaction.tx.commit().await?;
Ok((StatusCode::OK, Json(applications)))
}

pub async fn create_offer(
Path(id): Path<i64>,
State(state): State<AppState>,
_admin: CampaignAdmin,
mut transaction: DBTransaction<'_>,
Json(data): Json<Offer>,
) -> Result<impl IntoResponse, ChaosError> {
let _ = Offer::create(
id,
data.application_id,
data.email_template_id,
data.role_id,
data.expiry,
&mut transaction.tx,
state.snowflake_generator,
)
.await?;
transaction.tx.commit().await?;

Ok((StatusCode::OK, "Successfully created offer"))
}

pub async fn get_offers(
mut transaction: DBTransaction<'_>,
Path(id): Path<i64>,
_user: CampaignAdmin,
) -> Result<impl IntoResponse, ChaosError> {
let offers = Offer::get_by_campaign(id, &mut transaction.tx).await?;
transaction.tx.commit().await?;

Ok((StatusCode::OK, Json(offers)))
}
}
42 changes: 42 additions & 0 deletions backend/server/src/handler/email_template.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use crate::models::app::AppState;
use crate::models::auth::EmailTemplateAdmin;
use crate::models::email_template::EmailTemplate;
use crate::models::error::ChaosError;
use crate::models::transaction::DBTransaction;
use axum::extract::{Json, Path, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;

pub struct EmailTemplateHandler;
impl EmailTemplateHandler {
pub async fn get(
mut transaction: DBTransaction<'_>,
Path(id): Path<i64>,
_user: EmailTemplateAdmin,
) -> Result<impl IntoResponse, ChaosError> {
let email_template = EmailTemplate::get(id, &mut transaction.tx).await?;

Ok((StatusCode::OK, Json(email_template)))
}

pub async fn update(
_user: EmailTemplateAdmin,
Path(id): Path<i64>,
State(state): State<AppState>,
Json(request_body): Json<EmailTemplate>,
) -> Result<impl IntoResponse, ChaosError> {
EmailTemplate::update(id, request_body.name, request_body.template, &state.db).await?;

Ok((StatusCode::OK, "Successfully updated email template"))
}

pub async fn delete(
_user: EmailTemplateAdmin,
Path(id): Path<i64>,
State(state): State<AppState>,
) -> Result<impl IntoResponse, ChaosError> {
EmailTemplate::delete(id, &state.db).await?;

Ok((StatusCode::OK, "Successfully delete email template"))
}
}
2 changes: 2 additions & 0 deletions backend/server/src/handler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ pub mod answer;
pub mod application;
pub mod auth;
pub mod campaign;
pub mod email_template;
pub mod offer;
pub mod organisation;
pub mod question;
pub mod rating;
Expand Down
66 changes: 66 additions & 0 deletions backend/server/src/handler/offer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use crate::models::auth::{OfferAdmin, OfferRecipient};
use crate::models::error::ChaosError;
use crate::models::offer::{Offer, OfferReply};
use crate::models::transaction::DBTransaction;
use axum::extract::{Json, Path};
use axum::http::StatusCode;
use axum::response::IntoResponse;

pub struct OfferHandler;
impl OfferHandler {
pub async fn get(
mut transaction: DBTransaction<'_>,
Path(id): Path<i64>,
_user: OfferAdmin,
) -> Result<impl IntoResponse, ChaosError> {
let offer = Offer::get(id, &mut transaction.tx).await?;
transaction.tx.commit().await?;

Ok((StatusCode::OK, Json(offer)))
}

pub async fn delete(
mut transaction: DBTransaction<'_>,
Path(id): Path<i64>,
_user: OfferAdmin,
) -> Result<impl IntoResponse, ChaosError> {
Offer::delete(id, &mut transaction.tx).await?;
transaction.tx.commit().await?;

Ok((StatusCode::OK, "Successfully deleted offer"))
}

pub async fn reply(
mut transaction: DBTransaction<'_>,
Path(id): Path<i64>,
_user: OfferRecipient,
Json(reply): Json<OfferReply>,
) -> Result<impl IntoResponse, ChaosError> {
Offer::reply(id, reply.accept, &mut transaction.tx).await?;
transaction.tx.commit().await?;

Ok((StatusCode::OK, "Successfully accepted offer"))
}

pub async fn preview_email(
mut transaction: DBTransaction<'_>,
Path(id): Path<i64>,
_user: OfferAdmin,
) -> Result<impl IntoResponse, ChaosError> {
let string = Offer::preview_email(id, &mut transaction.tx).await?;
transaction.tx.commit().await?;

Ok((StatusCode::OK, string))
}

pub async fn send_offer(
mut transaction: DBTransaction<'_>,
Path(id): Path<i64>,
_user: OfferAdmin,
) -> Result<impl IntoResponse, ChaosError> {
Offer::send_offer(id, &mut transaction.tx).await?;
transaction.tx.commit().await?;

Ok((StatusCode::OK, "Successfully sent offer"))
}
}
Loading

0 comments on commit 2ec5c18

Please sign in to comment.