660 lines
21 KiB
Rust
660 lines
21 KiB
Rust
use crate::AppState;
|
|
use axum::{
|
|
extract::{Path, State},
|
|
http::StatusCode,
|
|
response::IntoResponse,
|
|
routing::{get, patch, post},
|
|
Json, Router,
|
|
};
|
|
use contracts::auth_middleware::AuthUser;
|
|
use serde::{Deserialize, Serialize};
|
|
use uuid::Uuid;
|
|
|
|
// ── Routers ───────────────────────────────────────────────────────────────────
|
|
|
|
pub fn coupons_router() -> Router<AppState> {
|
|
Router::new()
|
|
.route("/", get(list_coupons).post(create_coupon))
|
|
.route("/{id}", patch(update_coupon).delete(delete_coupon))
|
|
.route("/validate", post(validate_coupon))
|
|
}
|
|
|
|
pub fn discounts_router() -> Router<AppState> {
|
|
Router::new()
|
|
.route("/", get(list_discounts).post(create_discount))
|
|
.route("/{id}", patch(update_discount))
|
|
}
|
|
|
|
// ── Coupon DTOs ───────────────────────────────────────────────────────────────
|
|
|
|
#[derive(Serialize)]
|
|
struct CouponDto {
|
|
id: Uuid,
|
|
code: String,
|
|
title: Option<String>,
|
|
#[serde(rename = "type")]
|
|
discount_type: String,
|
|
value: i32,
|
|
min_order_amount: i32,
|
|
usage_limit: Option<i32>,
|
|
used_count: i32,
|
|
role_keys: Vec<String>,
|
|
applies_to: String,
|
|
is_active: bool,
|
|
valid_until: Option<chrono::DateTime<chrono::Utc>>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct CreateCouponBody {
|
|
code: String,
|
|
title: Option<String>,
|
|
#[serde(rename = "type")]
|
|
discount_type: Option<String>,
|
|
value: Option<i32>,
|
|
min_order_amount: Option<i32>,
|
|
max_uses: Option<i32>,
|
|
role_keys: Option<Vec<String>>,
|
|
applies_to: Option<String>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct PatchCouponBody {
|
|
code: Option<String>,
|
|
title: Option<String>,
|
|
#[serde(rename = "type")]
|
|
discount_type: Option<String>,
|
|
value: Option<i32>,
|
|
min_order_amount: Option<i32>,
|
|
max_uses: Option<i32>,
|
|
role_keys: Option<Vec<String>>,
|
|
is_active: Option<bool>,
|
|
}
|
|
|
|
// ── Discount DTOs ─────────────────────────────────────────────────────────────
|
|
|
|
#[derive(Serialize)]
|
|
struct DiscountDto {
|
|
id: Uuid,
|
|
title: String,
|
|
scope: String,
|
|
role_key: Option<String>,
|
|
package_id: Option<Uuid>,
|
|
#[serde(rename = "type")]
|
|
discount_type: String,
|
|
value: i32,
|
|
is_active: bool,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct CreateDiscountBody {
|
|
title: String,
|
|
scope: Option<String>,
|
|
role_key: Option<String>,
|
|
package_id: Option<Uuid>,
|
|
#[serde(rename = "type")]
|
|
discount_type: Option<String>,
|
|
value: Option<i32>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct PatchDiscountBody {
|
|
title: Option<String>,
|
|
scope: Option<String>,
|
|
role_key: Option<String>,
|
|
package_id: Option<Uuid>,
|
|
#[serde(rename = "type")]
|
|
discount_type: Option<String>,
|
|
value: Option<i32>,
|
|
is_active: Option<bool>,
|
|
}
|
|
|
|
// ── FromRow structs ──────────────────────────────────────────────────────────
|
|
|
|
#[derive(sqlx::FromRow)]
|
|
struct CouponRow {
|
|
id: Uuid,
|
|
code: String,
|
|
title: Option<String>,
|
|
discount_type: String,
|
|
discount_value: i32,
|
|
min_order_amount: i32,
|
|
max_uses: Option<i32>,
|
|
uses_count: i32,
|
|
role_keys: Vec<String>,
|
|
applies_to: String,
|
|
is_active: bool,
|
|
valid_until: Option<chrono::DateTime<chrono::Utc>>,
|
|
}
|
|
|
|
#[derive(sqlx::FromRow)]
|
|
struct ExistingCouponRow {
|
|
code: String,
|
|
title: Option<String>,
|
|
discount_type: String,
|
|
discount_value: i32,
|
|
min_order_amount: i32,
|
|
max_uses: Option<i32>,
|
|
role_keys: Vec<String>,
|
|
is_active: bool,
|
|
}
|
|
|
|
#[derive(sqlx::FromRow)]
|
|
struct ValidateCouponRow {
|
|
id: Uuid,
|
|
code: String,
|
|
discount_type: String,
|
|
discount_value: i32,
|
|
min_order_amount: i32,
|
|
max_uses: Option<i32>,
|
|
role_keys: Vec<String>,
|
|
valid_until: Option<chrono::DateTime<chrono::Utc>>,
|
|
is_active: bool,
|
|
}
|
|
|
|
#[derive(sqlx::FromRow)]
|
|
struct DiscountRow {
|
|
id: Uuid,
|
|
title: String,
|
|
scope: String,
|
|
role_key: Option<String>,
|
|
package_id: Option<Uuid>,
|
|
discount_type: String,
|
|
discount_value: i32,
|
|
is_active: bool,
|
|
}
|
|
|
|
#[derive(sqlx::FromRow)]
|
|
struct ExistingDiscountRow {
|
|
title: String,
|
|
scope: String,
|
|
role_key: Option<String>,
|
|
package_id: Option<Uuid>,
|
|
discount_type: String,
|
|
discount_value: i32,
|
|
is_active: bool,
|
|
}
|
|
|
|
// ── Coupon handlers ───────────────────────────────────────────────────────────
|
|
|
|
async fn list_coupons(
|
|
_auth: AuthUser,
|
|
State(state): State<AppState>,
|
|
) -> impl IntoResponse {
|
|
let rows = sqlx::query_as::<_, CouponRow>(
|
|
r#"
|
|
SELECT id, code, title, discount_type, discount_value, min_order_amount,
|
|
max_uses, uses_count, role_keys, applies_to, is_active, valid_until
|
|
FROM coupons
|
|
ORDER BY created_at DESC
|
|
"#,
|
|
)
|
|
.fetch_all(&state.pool)
|
|
.await;
|
|
|
|
match rows {
|
|
Ok(rows) => {
|
|
let dtos: Vec<CouponDto> = rows
|
|
.into_iter()
|
|
.map(|r| CouponDto {
|
|
id: r.id,
|
|
code: r.code,
|
|
title: r.title,
|
|
discount_type: r.discount_type,
|
|
value: r.discount_value,
|
|
min_order_amount: r.min_order_amount,
|
|
usage_limit: r.max_uses,
|
|
used_count: r.uses_count,
|
|
role_keys: r.role_keys,
|
|
applies_to: r.applies_to,
|
|
is_active: r.is_active,
|
|
valid_until: r.valid_until,
|
|
})
|
|
.collect();
|
|
(StatusCode::OK, Json(serde_json::json!({ "coupons": dtos }))).into_response()
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to list coupons: {e}");
|
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "Failed to load coupons" }))).into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn create_coupon(
|
|
_auth: AuthUser,
|
|
State(state): State<AppState>,
|
|
Json(body): Json<CreateCouponBody>,
|
|
) -> impl IntoResponse {
|
|
let discount_type = body.discount_type.unwrap_or_else(|| "PERCENT".to_string());
|
|
let value = body.value.unwrap_or(0);
|
|
let min_order = body.min_order_amount.unwrap_or(0);
|
|
let applies_to = body.applies_to.unwrap_or_else(|| "ALL".to_string());
|
|
let role_keys: Vec<String> = body.role_keys.unwrap_or_default();
|
|
let code = body.code.to_uppercase();
|
|
|
|
let row = sqlx::query_as::<_, CouponRow>(
|
|
r#"
|
|
INSERT INTO coupons (code, title, discount_type, discount_value, min_order_amount,
|
|
max_uses, role_keys, applies_to)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
RETURNING id, code, title, discount_type, discount_value, min_order_amount,
|
|
max_uses, uses_count, role_keys, applies_to, is_active, valid_until
|
|
"#,
|
|
)
|
|
.bind(&code)
|
|
.bind(&body.title)
|
|
.bind(&discount_type)
|
|
.bind(value)
|
|
.bind(min_order)
|
|
.bind(body.max_uses)
|
|
.bind(&role_keys)
|
|
.bind(&applies_to)
|
|
.fetch_one(&state.pool)
|
|
.await;
|
|
|
|
match row {
|
|
Ok(r) => {
|
|
let dto = CouponDto {
|
|
id: r.id,
|
|
code: r.code,
|
|
title: r.title,
|
|
discount_type: r.discount_type,
|
|
value: r.discount_value,
|
|
min_order_amount: r.min_order_amount,
|
|
usage_limit: r.max_uses,
|
|
used_count: r.uses_count,
|
|
role_keys: r.role_keys,
|
|
applies_to: r.applies_to,
|
|
is_active: r.is_active,
|
|
valid_until: r.valid_until,
|
|
};
|
|
(StatusCode::CREATED, Json(serde_json::json!(dto))).into_response()
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to create coupon: {e}");
|
|
if e.to_string().contains("unique") {
|
|
(StatusCode::CONFLICT, Json(serde_json::json!({ "error": "Coupon code already exists" }))).into_response()
|
|
} else {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "Failed to create coupon" }))).into_response()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn update_coupon(
|
|
_auth: AuthUser,
|
|
State(state): State<AppState>,
|
|
Path(id): Path<Uuid>,
|
|
Json(body): Json<PatchCouponBody>,
|
|
) -> impl IntoResponse {
|
|
let existing = sqlx::query_as::<_, ExistingCouponRow>(
|
|
"SELECT code, title, discount_type, discount_value, min_order_amount, max_uses, role_keys, is_active FROM coupons WHERE id = $1",
|
|
)
|
|
.bind(id)
|
|
.fetch_optional(&state.pool)
|
|
.await;
|
|
|
|
let existing = match existing {
|
|
Ok(Some(r)) => r,
|
|
Ok(None) => return (StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "Coupon not found" }))).into_response(),
|
|
Err(e) => {
|
|
tracing::error!("DB error: {e}");
|
|
return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "DB error" }))).into_response();
|
|
}
|
|
};
|
|
|
|
let code = body.code.map(|c| c.to_uppercase()).unwrap_or(existing.code);
|
|
let title = body.title.or(existing.title);
|
|
let discount_type = body.discount_type.unwrap_or(existing.discount_type);
|
|
let value = body.value.unwrap_or(existing.discount_value);
|
|
let min_order = body.min_order_amount.unwrap_or(existing.min_order_amount);
|
|
let max_uses = body.max_uses.or(existing.max_uses);
|
|
let role_keys = body.role_keys.unwrap_or(existing.role_keys);
|
|
let is_active = body.is_active.unwrap_or(existing.is_active);
|
|
|
|
let result = sqlx::query(
|
|
r#"
|
|
UPDATE coupons
|
|
SET code = $1, title = $2, discount_type = $3, discount_value = $4,
|
|
min_order_amount = $5, max_uses = $6, role_keys = $7, is_active = $8
|
|
WHERE id = $9
|
|
"#,
|
|
)
|
|
.bind(code)
|
|
.bind(title)
|
|
.bind(discount_type)
|
|
.bind(value)
|
|
.bind(min_order)
|
|
.bind(max_uses)
|
|
.bind(&role_keys)
|
|
.bind(is_active)
|
|
.bind(id)
|
|
.execute(&state.pool)
|
|
.await;
|
|
|
|
match result {
|
|
Ok(_) => (StatusCode::OK, Json(serde_json::json!({ "ok": true }))).into_response(),
|
|
Err(e) => {
|
|
tracing::error!("Failed to update coupon {id}: {e}");
|
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "Failed to update coupon" }))).into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn delete_coupon(
|
|
_auth: AuthUser,
|
|
State(state): State<AppState>,
|
|
Path(id): Path<Uuid>,
|
|
) -> impl IntoResponse {
|
|
let result = sqlx::query("DELETE FROM coupons WHERE id = $1")
|
|
.bind(id)
|
|
.execute(&state.pool)
|
|
.await;
|
|
|
|
match result {
|
|
Ok(r) if r.rows_affected() == 0 => {
|
|
(StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "Coupon not found" }))).into_response()
|
|
}
|
|
Ok(_) => StatusCode::NO_CONTENT.into_response(),
|
|
Err(e) => {
|
|
tracing::error!("Failed to delete coupon {id}: {e}");
|
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "Failed to delete coupon" }))).into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Discount handlers ─────────────────────────────────────────────────────────
|
|
|
|
async fn list_discounts(
|
|
_auth: AuthUser,
|
|
State(state): State<AppState>,
|
|
) -> impl IntoResponse {
|
|
let rows = sqlx::query_as::<_, DiscountRow>(
|
|
r#"
|
|
SELECT id, title, scope, role_key, package_id, discount_type, discount_value, is_active
|
|
FROM discounts
|
|
ORDER BY created_at DESC
|
|
"#,
|
|
)
|
|
.fetch_all(&state.pool)
|
|
.await;
|
|
|
|
match rows {
|
|
Ok(rows) => {
|
|
let dtos: Vec<DiscountDto> = rows
|
|
.into_iter()
|
|
.map(|r| DiscountDto {
|
|
id: r.id,
|
|
title: r.title,
|
|
scope: r.scope,
|
|
role_key: r.role_key,
|
|
package_id: r.package_id,
|
|
discount_type: r.discount_type,
|
|
value: r.discount_value,
|
|
is_active: r.is_active,
|
|
})
|
|
.collect();
|
|
(StatusCode::OK, Json(serde_json::json!({ "discounts": dtos }))).into_response()
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to list discounts: {e}");
|
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "Failed to load discounts" }))).into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn create_discount(
|
|
_auth: AuthUser,
|
|
State(state): State<AppState>,
|
|
Json(body): Json<CreateDiscountBody>,
|
|
) -> impl IntoResponse {
|
|
let scope = body.scope.unwrap_or_else(|| "ROLE".to_string());
|
|
let discount_type = body.discount_type.unwrap_or_else(|| "PERCENT".to_string());
|
|
let value = body.value.unwrap_or(0);
|
|
|
|
let row = sqlx::query_as::<_, DiscountRow>(
|
|
r#"
|
|
INSERT INTO discounts (title, scope, role_key, package_id, discount_type, discount_value)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
RETURNING id, title, scope, role_key, package_id, discount_type, discount_value, is_active
|
|
"#,
|
|
)
|
|
.bind(&body.title)
|
|
.bind(&scope)
|
|
.bind(&body.role_key)
|
|
.bind(body.package_id)
|
|
.bind(&discount_type)
|
|
.bind(value)
|
|
.fetch_one(&state.pool)
|
|
.await;
|
|
|
|
match row {
|
|
Ok(r) => {
|
|
let dto = DiscountDto {
|
|
id: r.id,
|
|
title: r.title,
|
|
scope: r.scope,
|
|
role_key: r.role_key,
|
|
package_id: r.package_id,
|
|
discount_type: r.discount_type,
|
|
value: r.discount_value,
|
|
is_active: r.is_active,
|
|
};
|
|
(StatusCode::CREATED, Json(serde_json::json!(dto))).into_response()
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to create discount: {e}");
|
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "Failed to create discount" }))).into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn update_discount(
|
|
_auth: AuthUser,
|
|
State(state): State<AppState>,
|
|
Path(id): Path<Uuid>,
|
|
Json(body): Json<PatchDiscountBody>,
|
|
) -> impl IntoResponse {
|
|
let existing = sqlx::query_as::<_, ExistingDiscountRow>(
|
|
"SELECT title, scope, role_key, package_id, discount_type, discount_value, is_active FROM discounts WHERE id = $1",
|
|
)
|
|
.bind(id)
|
|
.fetch_optional(&state.pool)
|
|
.await;
|
|
|
|
let existing = match existing {
|
|
Ok(Some(r)) => r,
|
|
Ok(None) => return (StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "Discount not found" }))).into_response(),
|
|
Err(e) => {
|
|
tracing::error!("DB error: {e}");
|
|
return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "DB error" }))).into_response();
|
|
}
|
|
};
|
|
|
|
let title = body.title.unwrap_or(existing.title);
|
|
let scope = body.scope.unwrap_or(existing.scope);
|
|
let role_key = body.role_key.or(existing.role_key);
|
|
let package_id = body.package_id.or(existing.package_id);
|
|
let discount_type = body.discount_type.unwrap_or(existing.discount_type);
|
|
let value = body.value.unwrap_or(existing.discount_value);
|
|
let is_active = body.is_active.unwrap_or(existing.is_active);
|
|
|
|
let result = sqlx::query(
|
|
r#"
|
|
UPDATE discounts
|
|
SET title = $1, scope = $2, role_key = $3, package_id = $4,
|
|
discount_type = $5, discount_value = $6, is_active = $7
|
|
WHERE id = $8
|
|
"#,
|
|
)
|
|
.bind(title)
|
|
.bind(scope)
|
|
.bind(role_key)
|
|
.bind(package_id)
|
|
.bind(discount_type)
|
|
.bind(value)
|
|
.bind(is_active)
|
|
.bind(id)
|
|
.execute(&state.pool)
|
|
.await;
|
|
|
|
match result {
|
|
Ok(_) => (StatusCode::OK, Json(serde_json::json!({ "updated": true }))).into_response(),
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response(),
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct ValidateCouponPayload {
|
|
coupon_code: String,
|
|
role_key: String,
|
|
package_price_inr: i32,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct ValidateCouponResponse {
|
|
valid: bool,
|
|
discount_type: Option<String>,
|
|
discount_value: Option<i32>,
|
|
final_price_inr: i32,
|
|
message: String,
|
|
}
|
|
|
|
async fn validate_coupon(
|
|
_auth: AuthUser,
|
|
State(state): State<AppState>,
|
|
Json(payload): Json<ValidateCouponPayload>,
|
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
|
let code = payload.coupon_code.trim().to_uppercase();
|
|
|
|
// Fetch coupon
|
|
let coupon = sqlx::query_as::<_, ValidateCouponRow>(
|
|
r#"
|
|
SELECT id, code, discount_type, discount_value, min_order_amount,
|
|
max_uses, role_keys, valid_until, is_active
|
|
FROM coupons
|
|
WHERE code = $1
|
|
"#,
|
|
)
|
|
.bind(&code)
|
|
.fetch_optional(&state.pool)
|
|
.await
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
|
|
|
let coupon = match coupon {
|
|
Some(c) => c,
|
|
None => {
|
|
return Ok((
|
|
StatusCode::OK,
|
|
Json(ValidateCouponResponse {
|
|
valid: false,
|
|
discount_type: None,
|
|
discount_value: None,
|
|
final_price_inr: payload.package_price_inr,
|
|
message: "Coupon not found".to_string(),
|
|
}),
|
|
));
|
|
}
|
|
};
|
|
|
|
// Check active
|
|
if !coupon.is_active {
|
|
return Ok((
|
|
StatusCode::OK,
|
|
Json(ValidateCouponResponse {
|
|
valid: false,
|
|
discount_type: None,
|
|
discount_value: None,
|
|
final_price_inr: payload.package_price_inr,
|
|
message: "Coupon is inactive".to_string(),
|
|
}),
|
|
));
|
|
}
|
|
|
|
// Check expiry
|
|
if let Some(valid_until) = coupon.valid_until {
|
|
if valid_until < chrono::Utc::now() {
|
|
return Ok((
|
|
StatusCode::OK,
|
|
Json(ValidateCouponResponse {
|
|
valid: false,
|
|
discount_type: None,
|
|
discount_value: None,
|
|
final_price_inr: payload.package_price_inr,
|
|
message: "Coupon has expired".to_string(),
|
|
}),
|
|
));
|
|
}
|
|
}
|
|
|
|
// Check role restriction
|
|
if !coupon.role_keys.is_empty() && !coupon.role_keys.iter().any(|r| r.eq_ignore_ascii_case(&payload.role_key)) {
|
|
return Ok((
|
|
StatusCode::OK,
|
|
Json(ValidateCouponResponse {
|
|
valid: false,
|
|
discount_type: None,
|
|
discount_value: None,
|
|
final_price_inr: payload.package_price_inr,
|
|
message: "Coupon not valid for your role".to_string(),
|
|
}),
|
|
));
|
|
}
|
|
|
|
// Check minimum order amount
|
|
if payload.package_price_inr < coupon.min_order_amount {
|
|
return Ok((
|
|
StatusCode::OK,
|
|
Json(ValidateCouponResponse {
|
|
valid: false,
|
|
discount_type: None,
|
|
discount_value: None,
|
|
final_price_inr: payload.package_price_inr,
|
|
message: format!("Minimum order amount ₹{} required", coupon.min_order_amount / 100),
|
|
}),
|
|
));
|
|
}
|
|
|
|
// Check usage limit if set
|
|
if let Some(max_uses) = coupon.max_uses {
|
|
let count: i64 = sqlx::query_scalar::<_, i64>(
|
|
"SELECT COUNT(*) FROM coupon_uses WHERE coupon_id = $1",
|
|
)
|
|
.bind(coupon.id)
|
|
.fetch_one(&state.pool)
|
|
.await
|
|
.unwrap_or(0);
|
|
if count >= max_uses as i64 {
|
|
return Ok((
|
|
StatusCode::OK,
|
|
Json(ValidateCouponResponse {
|
|
valid: false,
|
|
discount_type: None,
|
|
discount_value: None,
|
|
final_price_inr: payload.package_price_inr,
|
|
message: "Coupon usage limit reached".to_string(),
|
|
}),
|
|
));
|
|
}
|
|
}
|
|
|
|
// Compute final price
|
|
let final_price = match coupon.discount_type.as_str() {
|
|
"PERCENT" => {
|
|
let discount = ((payload.package_price_inr as f64) * (coupon.discount_value as f64) / 100.0).round() as i32;
|
|
(payload.package_price_inr - discount).max(0)
|
|
}
|
|
"FIXED" => (payload.package_price_inr - coupon.discount_value).max(0),
|
|
_ => payload.package_price_inr,
|
|
};
|
|
|
|
Ok((
|
|
StatusCode::OK,
|
|
Json(ValidateCouponResponse {
|
|
valid: true,
|
|
discount_type: Some(coupon.discount_type),
|
|
discount_value: Some(coupon.discount_value),
|
|
final_price_inr: final_price,
|
|
message: "Coupon applied".to_string(),
|
|
}),
|
|
))
|
|
}
|