Step 5 — signature is a modular step

sent_for_signature__pending is one state in the graph, and the thing that fires it is a small trait — not a vendor. DocuSign is the shipped implementation; dev and tests run a recording stub, so the step stays testable without an account.

From portal/src/signature.rs:


pub trait SignatureProvider: Send + Sync {
    /// Submit the rendered retainer PDF for the given notation, placing
    /// the fields described by `manifest`. Returns a provider-issued id
    /// correlating future events.
    async fn send_for_signature(
        &self,
        notation_id: Uuid,
        pdf: &[u8],
        manifest: &SignatureManifest,
    ) -> Result<SignatureRequestId, SignatureError>;

Because the step is modular it can also be careful. Dispatch is idempotent — a notation that already has an envelope out reuses it, fires nothing, and sends nothing — so a retry can never double-send a client's contract.

From portal/src/retainer_walk.rs:


    // Idempotency: this notation already has an envelope out. Reuse the
    // recorded id, fire nothing, send nothing — the post-state is
    // whatever the notation already records.
    if let Some(existing) =
        store::signatures::request_id_for_notation(deps.surreal, notation_id).await?
    {
        return Ok((
            StateName::from(notation_row.state.as_str()),
            crate::signature::SignatureRequestId(existing),
        ));
    }
‹✕›