dhan-rs 0.1.7

Unofficial Rust client library for the DhanHQ Broker API v2 — orders, market data, WebSocket feeds, and more
Documentation
#![allow(missing_docs)]
//! Postback (webhook) payload types.
//!
//! When you configure a Postback URL during access-token generation, Dhan sends
//! a `POST` request with a JSON payload to your URL whenever an order changes
//! status (`TRANSIT`, `PENDING`, `REJECTED`, `CANCELLED`, `TRADED`, `EXPIRED`)
//! or when an order is modified or partially filled.
//!
//! This module provides the [`PostbackPayload`] struct for deserializing those
//! incoming webhook bodies. It does **not** make any outgoing API calls — it
//! is purely for users who host their own HTTP server to receive postbacks.
//!
//! # Example
//!
//! ```no_run
//! use dhan_rs::types::postback::PostbackPayload;
//!
//! // In your web-server handler:
//! fn handle_postback(body: &str) {
//!     let payload: PostbackPayload = serde_json::from_str(body).unwrap();
//!     println!("Order {} status: {:?}", payload.order_id.unwrap_or_default(), payload.order_status);
//! }
//! ```

use serde::Deserialize;

/// Reason a deserialized postback is not a minimally usable order update.
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum PostbackValidationError {
    /// The payload has no non-blank Dhan client identity.
    #[error("postback dhan_client_id is required")]
    MissingClientId,
    /// The payload has no non-blank order identity.
    #[error("postback order_id is required")]
    MissingOrderId,
    /// The payload has no non-blank order status.
    #[error("postback order_status is required")]
    MissingOrderStatus,
}

/// Postback (webhook) payload sent by Dhan to your configured Postback URL.
///
/// The JSON body is a raw `POST` request containing the order update. Fields
/// are largely identical to [`crate::types::orders::OrderDetail`] but the wire
/// format mixes camelCase with the snake_case field `filled_qty`.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PostbackPayload {
    /// User-specific identification generated by Dhan.
    pub dhan_client_id: Option<String>,

    /// Order-specific identification generated by Dhan.
    pub order_id: Option<String>,

    /// User/partner generated tracking ID.
    pub correlation_id: Option<String>,

    /// Last updated status of the order.
    ///
    /// One of: `TRANSIT`, `PENDING`, `REJECTED`, `CANCELLED`, `TRADED`, `EXPIRED`.
    pub order_status: Option<String>,

    /// The trading side of the transaction (`BUY` or `SELL`).
    pub transaction_type: Option<String>,

    /// Exchange & segment (e.g. `NSE_EQ`, `NSE_FNO`, `BSE_EQ`, `MCX_COMM`).
    pub exchange_segment: Option<String>,

    /// Product type (`CNC`, `INTRADAY`, `MARGIN`, `MTF`, `CO`, `BO`).
    pub product_type: Option<String>,

    /// Order type (`LIMIT`, `MARKET`, `STOP_LOSS`, `STOP_LOSS_MARKET`).
    pub order_type: Option<String>,

    /// Order validity (`DAY`, `IOC`).
    pub validity: Option<String>,

    /// Trading symbol of the instrument.
    pub trading_symbol: Option<String>,

    /// Exchange standard security ID.
    pub security_id: Option<String>,

    /// Number of shares for the order.
    #[serde(default)]
    pub quantity: Option<u64>,

    /// Number of shares disclosed/visible.
    #[serde(default)]
    pub disclosed_quantity: Option<u64>,

    /// Price at which the order was placed.
    #[serde(default)]
    pub price: Option<f64>,

    /// Trigger price for SL/SL-M/CO/BO orders.
    #[serde(default)]
    pub trigger_price: Option<f64>,

    /// Whether this is an after-market order.
    #[serde(default)]
    pub after_market_order: Option<bool>,

    /// Bracket order target price change.
    #[serde(default)]
    pub bo_profit_value: Option<f64>,

    /// Bracket order stop-loss price change.
    #[serde(default)]
    pub bo_stop_loss_value: Option<f64>,

    /// Leg identification for BO/CO orders (`ENTRY_LEG`, `TARGET_LEG`, `STOP_LOSS_LEG`).
    pub leg_name: Option<String>,

    /// Time at which the order was created.
    pub create_time: Option<String>,

    /// Time at which the last activity happened.
    pub update_time: Option<String>,

    /// Time at which the order reached the exchange.
    pub exchange_time: Option<String>,

    /// Expiry date for F&O contracts.
    pub drv_expiry_date: Option<String>,

    /// Option type (`CALL` or `PUT`).
    pub drv_option_type: Option<String>,

    /// Strike price for Options.
    #[serde(default)]
    pub drv_strike_price: Option<f64>,

    /// OMS error code if the order was rejected or failed.
    ///
    /// Note: the API documentation shows this as `omsErrorCode` (camelCase).
    pub oms_error_code: Option<String>,

    /// Description of the OMS error.
    pub oms_error_description: Option<String>,

    /// Quantity already traded / filled.
    ///
    /// Note: This field arrives as snake_case `filled_qty` in the wire payload,
    /// unlike the rest of the camelCase fields.
    #[serde(default, alias = "filled_qty")]
    pub filled_qty: Option<u64>,

    /// Algo ID registered with the exchange (for registered algos).
    pub algo_id: Option<String>,
}

impl PostbackPayload {
    /// Validate the minimum identity and state fields required to process an
    /// order update safely.
    ///
    /// Deserialization intentionally remains permissive for compatibility;
    /// webhook receivers should call this before acting on a payload.
    pub fn validate(&self) -> Result<(), PostbackValidationError> {
        if self
            .dhan_client_id
            .as_deref()
            .is_none_or(|value| value.trim().is_empty())
        {
            return Err(PostbackValidationError::MissingClientId);
        }
        if self
            .order_id
            .as_deref()
            .is_none_or(|value| value.trim().is_empty())
        {
            return Err(PostbackValidationError::MissingOrderId);
        }
        if self
            .order_status
            .as_deref()
            .is_none_or(|value| value.trim().is_empty())
        {
            return Err(PostbackValidationError::MissingOrderStatus);
        }
        Ok(())
    }

    /// Returns `true` if this payload represents a fully traded order.
    pub fn is_traded(&self) -> bool {
        self.order_status.as_deref() == Some("TRADED")
    }

    /// Returns `true` if this payload represents a rejected order.
    pub fn is_rejected(&self) -> bool {
        self.order_status.as_deref() == Some("REJECTED")
    }

    /// Returns `true` if this payload represents a cancelled order.
    pub fn is_cancelled(&self) -> bool {
        self.order_status.as_deref() == Some("CANCELLED")
    }

    /// Returns `true` if this payload represents a pending order.
    pub fn is_pending(&self) -> bool {
        self.order_status.as_deref() == Some("PENDING")
    }

    /// Returns `true` if this is a buy transaction.
    pub fn is_buy(&self) -> bool {
        self.transaction_type.as_deref() == Some("BUY")
    }

    /// Returns `true` if this is a sell transaction.
    pub fn is_sell(&self) -> bool {
        self.transaction_type.as_deref() == Some("SELL")
    }
}