use super::idempotency::IdempotencyKey;
use crate::internal::domain::{AccountId, BrokerOrderId, ErrorCode, GatewayError, ValidatedOrder};
use crate::internal::orders::OrderModifyFields;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct PaperSubmitReceipt {
pub broker_order_id: BrokerOrderId,
pub broker_status: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct PaperCancelReceipt {
pub broker_order_id: BrokerOrderId,
pub accepted: bool,
pub broker_status: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct PaperModifyReceipt {
pub broker_order_id: BrokerOrderId,
pub accepted: bool,
pub broker_status: Option<String>,
}
#[async_trait]
pub trait PaperOrderWriter: Send + Sync {
async fn submit_paper(
&self,
order: &ValidatedOrder,
idempotency_key: &IdempotencyKey,
) -> Result<PaperSubmitReceipt, GatewayError>;
async fn cancel_paper(
&self,
account_id: &AccountId,
broker_order_id: &BrokerOrderId,
idempotency_key: &IdempotencyKey,
) -> Result<PaperCancelReceipt, GatewayError>;
async fn modify_paper(
&self,
account_id: &AccountId,
broker_order_id: &BrokerOrderId,
changes: &OrderModifyFields,
idempotency_key: &IdempotencyKey,
) -> Result<PaperModifyReceipt, GatewayError>;
}
#[derive(Clone, Debug, Default)]
pub struct LocalCandidatePaperWriter;
#[async_trait]
impl PaperOrderWriter for LocalCandidatePaperWriter {
async fn submit_paper(
&self,
_order: &ValidatedOrder,
_idempotency_key: &IdempotencyKey,
) -> Result<PaperSubmitReceipt, GatewayError> {
Ok(PaperSubmitReceipt {
broker_order_id: BrokerOrderId::from_static("paper-order-local"),
broker_status: Some("LocalCandidate".to_string()),
})
}
async fn cancel_paper(
&self,
_account_id: &AccountId,
broker_order_id: &BrokerOrderId,
_idempotency_key: &IdempotencyKey,
) -> Result<PaperCancelReceipt, GatewayError> {
Ok(PaperCancelReceipt {
broker_order_id: broker_order_id.clone(),
accepted: true,
broker_status: Some("LocalCandidate".to_string()),
})
}
async fn modify_paper(
&self,
_account_id: &AccountId,
broker_order_id: &BrokerOrderId,
_changes: &OrderModifyFields,
_idempotency_key: &IdempotencyKey,
) -> Result<PaperModifyReceipt, GatewayError> {
Ok(PaperModifyReceipt {
broker_order_id: broker_order_id.clone(),
accepted: true,
broker_status: Some("Modified".to_string()),
})
}
}
#[derive(Clone, Debug, Default)]
pub struct RefusingPaperWriter;
#[async_trait]
impl PaperOrderWriter for RefusingPaperWriter {
async fn submit_paper(
&self,
_order: &ValidatedOrder,
_idempotency_key: &IdempotencyKey,
) -> Result<PaperSubmitReceipt, GatewayError> {
Err(refusing_error("submit"))
}
async fn cancel_paper(
&self,
_account_id: &AccountId,
_broker_order_id: &BrokerOrderId,
_idempotency_key: &IdempotencyKey,
) -> Result<PaperCancelReceipt, GatewayError> {
Err(refusing_error("cancel"))
}
async fn modify_paper(
&self,
_account_id: &AccountId,
_broker_order_id: &BrokerOrderId,
_changes: &OrderModifyFields,
_idempotency_key: &IdempotencyKey,
) -> Result<PaperModifyReceipt, GatewayError> {
Err(refusing_error("modify"))
}
}
fn refusing_error(operation: &str) -> GatewayError {
GatewayError::new(
ErrorCode::PaperTradingDisabled,
format!("No paper order writer is wired for {operation}"),
false,
Some("Configure a PaperOrderWriter (e.g. ClientPortalPaperWriter)".to_string()),
)
}
#[cfg(test)]
mod tests {
use super::{LocalCandidatePaperWriter, PaperOrderWriter, RefusingPaperWriter};
use crate::internal::domain::{AccountId, BrokerOrderId, ErrorCode};
use crate::internal::orders::IdempotencyKey;
#[tokio::test]
async fn local_candidate_cancel_echoes_broker_id() {
let writer = LocalCandidatePaperWriter;
let Ok(key) = IdempotencyKey::new("paper-key") else {
unreachable!("non-empty key");
};
let order_id = BrokerOrderId::from_static("paper-order-local");
let Ok(receipt) = writer
.cancel_paper(&AccountId::from_static("DU1234567"), &order_id, &key)
.await
else {
unreachable!("local candidate writer must succeed");
};
assert_eq!(receipt.broker_order_id, order_id);
assert!(receipt.accepted);
}
#[tokio::test]
async fn refusing_writer_refuses_cancel() {
let writer = RefusingPaperWriter;
let Ok(key) = IdempotencyKey::new("paper-key") else {
unreachable!("non-empty key");
};
let error = writer
.cancel_paper(
&AccountId::from_static("DU1234567"),
&BrokerOrderId::from_static("any"),
&key,
)
.await;
let Err(error) = error else {
unreachable!("RefusingPaperWriter must refuse cancel");
};
assert_eq!(error.code, ErrorCode::PaperTradingDisabled);
}
}