use reqwest::Method;
use serde::{Deserialize, Serialize};
use crate::error::Result;
use crate::http::HttpClient;
use crate::models::{
Assignment, AssignmentMethod, CostEstimate, NotificationMethod, ResendCostEstimate,
ResendNotificationResult, SignDocumentItem, VerificationMethod, WhatsAppNotification,
};
fn reset_expiration_payload(new_expires_at: Option<&str>) -> serde_json::Value {
serde_json::json!({ "expires_at": new_expires_at })
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CreateAssignmentSigner {
#[serde(skip_serializing_if = "String::is_empty", default)]
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub step: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub verification_method: Option<VerificationMethod>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notification_methods: Option<Vec<NotificationMethod>>,
}
impl CreateAssignmentSigner {
pub fn new<S: Into<String>>(signer_id: S) -> Self {
Self {
id: signer_id.into(),
..Default::default()
}
}
pub fn id<S: Into<String>>(mut self, signer_id: S) -> Self {
self.id = signer_id.into();
self
}
pub fn step(mut self, step: u32) -> Self {
self.step = Some(step);
self
}
pub fn verification_method(mut self, method: VerificationMethod) -> Self {
self.verification_method = Some(method);
self
}
pub fn notification_methods(mut self, methods: Vec<NotificationMethod>) -> Self {
self.notification_methods = Some(methods);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateAssignmentBody {
pub method: AssignmentMethod,
#[serde(
rename = "signer_ids",
alias = "signerIds",
skip_serializing_if = "Vec::is_empty",
default
)]
pub signer_ids: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signers: Option<Vec<CreateAssignmentSigner>>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub entries: Vec<AssignmentEntry>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expiration: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub verification_method: Option<VerificationMethod>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notification_methods: Option<Vec<NotificationMethod>>,
#[serde(
rename = "copy_receivers",
skip_serializing_if = "Vec::is_empty",
default
)]
pub copy_receiver_ids: Vec<String>,
}
impl CreateAssignmentBody {
pub fn new<I, S>(method: AssignmentMethod, signer_ids: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let signers = signer_ids
.into_iter()
.map(CreateAssignmentSigner::new)
.collect::<Vec<_>>();
Self::from_signers(method, signers)
}
pub fn from_signers<I>(method: AssignmentMethod, signers: I) -> Self
where
I: IntoIterator<Item = CreateAssignmentSigner>,
{
let signers = signers.into_iter().collect::<Vec<_>>();
Self {
method,
signer_ids: Vec::new(),
signers: (!signers.is_empty()).then_some(signers),
entries: Vec::new(),
message: None,
expiration: None,
expires_at: None,
verification_method: None,
notification_methods: None,
copy_receiver_ids: Vec::new(),
}
}
pub fn message<S: Into<String>>(mut self, message: S) -> Self {
self.message = Some(message.into());
self
}
pub fn expires_at<S: Into<String>>(mut self, ts: S) -> Self {
self.expires_at = Some(ts.into());
self
}
pub fn expiration<S: Into<String>>(mut self, ts: S) -> Self {
self.expiration = Some(ts.into());
self
}
pub fn with_signers(mut self, signers: Vec<CreateAssignmentSigner>) -> Self {
self.signers = Some(signers);
self.signer_ids.clear();
self
}
pub fn signers<I>(mut self, signers: I) -> Self
where
I: IntoIterator<Item = CreateAssignmentSigner>,
{
let signers = signers.into_iter().collect::<Vec<_>>();
self.signers = Some(signers);
self.signer_ids.clear();
self
}
pub fn legacy_signer_ids<I, S>(mut self, signer_ids: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.signer_ids = signer_ids.into_iter().map(Into::into).collect();
self.signers = None;
self
}
pub fn entries(mut self, entries: Vec<AssignmentEntry>) -> Self {
self.entries = entries;
self
}
pub fn verification_method(mut self, method: VerificationMethod) -> Self {
self.verification_method = Some(method);
self
}
pub fn notification_methods(mut self, methods: Vec<NotificationMethod>) -> Self {
self.notification_methods = Some(methods);
self
}
pub fn copy_receivers<I, S>(mut self, ids: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.copy_receiver_ids = ids.into_iter().map(Into::into).collect();
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssignmentEntry {
pub page_id: String,
pub fields: Vec<AssignmentField>,
}
impl AssignmentEntry {
pub fn new<S: Into<String>>(page_id: S, fields: Vec<AssignmentField>) -> Self {
Self {
page_id: page_id.into(),
fields,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssignmentField {
pub signer_id: String,
pub field_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_settings: Option<serde_json::Value>,
}
impl AssignmentField {
pub fn new<S, F>(signer_id: S, field_id: F) -> Self
where
S: Into<String>,
F: Into<String>,
{
Self {
signer_id: signer_id.into(),
field_id: field_id.into(),
display_settings: None,
}
}
pub fn display_settings(mut self, settings: impl Into<serde_json::Value>) -> Self {
self.display_settings = Some(settings.into());
self
}
}
pub type EstimateAssignmentCostBody = CreateAssignmentBody;
#[derive(Debug)]
pub struct AssignmentsApi<'a> {
http: &'a HttpClient,
}
impl<'a> AssignmentsApi<'a> {
pub(crate) fn new(http: &'a HttpClient) -> Self {
Self { http }
}
pub async fn create<S: AsRef<str>>(
&self,
document_id: S,
body: &CreateAssignmentBody,
) -> Result<Assignment> {
let path = format!("documents/{}/assignments", document_id.as_ref());
let req = self.http.request(Method::POST, &path)?.json(body);
self.http.send_data(req).await
}
pub async fn estimate_cost<S: AsRef<str>>(
&self,
document_id: S,
body: &EstimateAssignmentCostBody,
) -> Result<CostEstimate> {
let path = format!(
"documents/{}/assignments/estimate-cost",
document_id.as_ref()
);
let req = self.http.request(Method::POST, &path)?.json(body);
self.http.send_envelope(req).await
}
pub async fn reset_expiration<D: AsRef<str>, A: AsRef<str>>(
&self,
document_id: D,
assignment_id: A,
new_expires_at: Option<&str>,
) -> Result<Assignment> {
let path = format!(
"documents/{}/assignments/{}/reset-expiration",
document_id.as_ref(),
assignment_id.as_ref()
);
let req = self
.http
.request(Method::PUT, &path)?
.json(&reset_expiration_payload(new_expires_at));
self.http.send_envelope(req).await
}
pub async fn resend<D: AsRef<str>, A: AsRef<str>>(
&self,
document_id: D,
assignment_id: A,
) -> Result<Assignment> {
let path = format!(
"documents/{}/assignments/{}/resend",
document_id.as_ref(),
assignment_id.as_ref()
);
let req = self.http.request(Method::PUT, &path)?;
self.http.send_envelope(req).await
}
pub async fn resend_to_signer<D: AsRef<str>, A: AsRef<str>, S: AsRef<str>>(
&self,
document_id: D,
assignment_id: A,
signer_id: S,
) -> Result<ResendNotificationResult> {
let path = format!(
"documents/{}/assignments/{}/signers/{}/resend",
document_id.as_ref(),
assignment_id.as_ref(),
signer_id.as_ref()
);
let req = self.http.request(Method::PUT, &path)?;
self.http.send_envelope(req).await
}
pub async fn estimate_resend_cost<D: AsRef<str>, A: AsRef<str>, S: AsRef<str>>(
&self,
document_id: D,
assignment_id: A,
signer_id: S,
) -> Result<ResendCostEstimate> {
let path = format!(
"documents/{}/assignments/{}/signers/{}/estimate-resend-cost",
document_id.as_ref(),
assignment_id.as_ref(),
signer_id.as_ref()
);
let req = self.http.request(Method::POST, &path)?;
self.http.send_envelope(req).await
}
pub async fn whatsapp_notifications<D: AsRef<str>, A: AsRef<str>>(
&self,
document_id: D,
assignment_id: A,
) -> Result<Vec<WhatsAppNotification>> {
let path = format!(
"documents/{}/assignments/{}/whatsapp-notifications",
document_id.as_ref(),
assignment_id.as_ref()
);
let req = self.http.request(Method::GET, &path)?;
self.http.send_envelope(req).await
}
pub async fn sign<D: AsRef<str>, A: AsRef<str>, I>(
&self,
document_id: D,
assignment_id: A,
items: I,
) -> Result<()>
where
I: IntoIterator<Item = SignDocumentItem>,
{
let path = format!(
"documents/{}/assignments/{}",
document_id.as_ref(),
assignment_id.as_ref()
);
let items: Vec<SignDocumentItem> = items.into_iter().collect();
let req = self.http.request(Method::POST, &path)?.json(&items);
self.http.send_no_content(req).await
}
pub async fn reject<D: AsRef<str>, A: AsRef<str>, R: AsRef<str>>(
&self,
document_id: D,
assignment_id: A,
reason: R,
) -> Result<()> {
let path = format!(
"documents/{}/assignments/{}/reject",
document_id.as_ref(),
assignment_id.as_ref()
);
let req = self
.http
.request(Method::PUT, &path)?
.json(&serde_json::json!({ "decline_reason": reason.as_ref() }));
self.http.send_no_content(req).await
}
}
#[cfg(test)]
mod tests {
use super::reset_expiration_payload;
#[test]
fn reset_expiration_none_serializes_null() {
assert_eq!(
reset_expiration_payload(None),
serde_json::json!({ "expires_at": null })
);
}
#[test]
fn reset_expiration_some_serializes_timestamp() {
assert_eq!(
reset_expiration_payload(Some("2026-06-01T12:00:00Z")),
serde_json::json!({ "expires_at": "2026-06-01T12:00:00Z" })
);
}
}