1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
use serde::{Deserialize, Serialize};

use crate::client::Mpesa;
use crate::constants::CommandId;
use crate::environment::ApiEnvironment;
use crate::errors::{MpesaError, MpesaResult};

#[derive(Debug, Serialize)]
/// Payload to make payment requests from C2B.
/// See more: https://developer.safaricom.co.ke/docs#c2b-api
struct C2bSimulatePayload<'mpesa> {
    #[serde(rename(serialize = "CommandID"))]
    command_id: CommandId,
    #[serde(rename(serialize = "Amount"))]
    amount: f64,
    #[serde(rename(serialize = "Msisdn"))]
    msisdn: &'mpesa str,
    #[serde(rename(serialize = "BillRefNumber"))]
    bill_ref_number: &'mpesa str,
    #[serde(rename(serialize = "ShortCode"))]
    short_code: &'mpesa str,
}

#[derive(Debug, Clone, Deserialize)]
pub struct C2bSimulateResponse {
    #[serde(
        rename(deserialize = "ConversationID"),
        skip_serializing_if = "Option::is_none"
    )]
    pub conversation_id: Option<String>,
    #[serde(rename(deserialize = "OriginatorConversationID"))]
    pub originator_conversation_id: String,
    #[serde(rename(deserialize = "ResponseCode"))]
    pub response_code: String,
    #[serde(rename(deserialize = "ResponseDescription"))]
    pub response_description: String,
}

#[derive(Debug)]
pub struct C2bSimulateBuilder<'mpesa, Env: ApiEnvironment> {
    client: &'mpesa Mpesa<Env>,
    command_id: Option<CommandId>,
    amount: Option<f64>,
    msisdn: Option<&'mpesa str>,
    bill_ref_number: Option<&'mpesa str>,
    short_code: Option<&'mpesa str>,
}

impl<'mpesa, Env: ApiEnvironment> C2bSimulateBuilder<'mpesa, Env> {
    /// Creates a new C2B Simulate builder
    pub fn new(client: &'mpesa Mpesa<Env>) -> C2bSimulateBuilder<'mpesa, Env> {
        C2bSimulateBuilder {
            client,
            command_id: None,
            amount: None,
            msisdn: None,
            bill_ref_number: None,
            short_code: None,
        }
    }

    /// Adds `CommandId`. Defaults to `CommandId::CustomerPaybillOnline` if no value explicitly passed
    ///
    /// # Errors
    /// If `CommandId` is not valid
    pub fn command_id(mut self, command_id: CommandId) -> C2bSimulateBuilder<'mpesa, Env> {
        self.command_id = Some(command_id);
        self
    }

    /// Adds an `amount` to the request
    ///
    /// # Errors
    /// If `Amount` is not provided
    pub fn amount<Number: Into<f64>>(mut self, amount: Number) -> C2bSimulateBuilder<'mpesa, Env> {
        self.amount = Some(amount.into());
        self
    }

    /// Adds the MSISDN(phone number) sending the transaction, start by country code without the `+`.
    /// This is a required field
    ///
    /// # Errors
    /// If `MSISDN` is invalid or not provided
    pub fn msisdn(mut self, msisdn: &'mpesa str) -> C2bSimulateBuilder<'mpesa, Env> {
        self.msisdn = Some(msisdn);
        self
    }

    /// Adds `ShortCode`; the 6 digit MPESA Till Number or PayBill Number
    ///
    /// # Errors
    /// If Till or PayBill number is invalid or not provided
    pub fn short_code(mut self, short_code: &'mpesa str) -> C2bSimulateBuilder<'mpesa, Env> {
        self.short_code = Some(short_code);
        self
    }

    /// Adds Bill reference number.
    ///
    /// # Errors
    /// If `BillRefNumber` is invalid or not provided
    pub fn bill_ref_number(
        mut self,
        bill_ref_number: &'mpesa str,
    ) -> C2bSimulateBuilder<'mpesa, Env> {
        self.bill_ref_number = Some(bill_ref_number);
        self
    }

    /// # C2B Simulate API
    ///
    /// Make payment requests from Client to Business
    ///
    /// This enables you to receive the payment requests in real time.
    /// See more [here](https://developer.safaricom.co.ke/c2b/apis/post/simulate)
    ///
    /// A successful request returns a `C2bSimulateResponse` type
    ///
    /// # Errors
    /// Returns a `MpesaError` on failure
    #[allow(clippy::unnecessary_lazy_evaluations)]
    pub async fn send(self) -> MpesaResult<C2bSimulateResponse> {
        let url = format!(
            "{}/mpesa/c2b/v1/simulate",
            self.client.environment.base_url()
        );

        let payload = C2bSimulatePayload {
            command_id: self
                .command_id
                .unwrap_or_else(|| CommandId::CustomerPayBillOnline),
            amount: self
                .amount
                .ok_or(MpesaError::Message("amount is required"))?,
            msisdn: self
                .msisdn
                .ok_or(MpesaError::Message("msisdn is required"))?,
            bill_ref_number: self
                .bill_ref_number
                .ok_or(MpesaError::Message("bill_ref_number is required"))?,
            short_code: self
                .short_code
                .ok_or(MpesaError::Message("short_code is required"))?,
        };

        let response = self
            .client
            .http_client
            .post(&url)
            .bearer_auth(self.client.auth().await?)
            .json(&payload)
            .send()
            .await?;

        if response.status().is_success() {
            let value = response.json::<_>().await?;
            return Ok(value);
        }

        let value = response.json().await?;
        Err(MpesaError::C2bSimulateError(value))
    }
}