1use serde::{Deserialize, Serialize};
16
17use super::{InternalVerb, VerbCategory};
18
19pub struct PaymentsChargeCreate;
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
27pub struct PaymentsChargeCreateInput {
28 pub amount_cents: u64,
30 pub currency: String,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub description: Option<String>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub customer_id: Option<String>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
44pub struct PaymentsChargeCreateOutput {
45 pub id: String,
47 pub status: String,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub client_secret: Option<String>,
54}
55
56impl InternalVerb for PaymentsChargeCreate {
57 type Input = PaymentsChargeCreateInput;
58 type Output = PaymentsChargeCreateOutput;
59 const ID: &'static str = "payments.charge.create";
60 const CATEGORY: VerbCategory = VerbCategory::Payments;
61}
62
63pub struct PaymentsSubscriptionUpsert;
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
71pub struct PaymentsSubscriptionUpsertInput {
72 pub customer_id: String,
74 pub price_id: String,
77 #[serde(default = "quantity_one")]
79 pub quantity: u32,
80}
81
82fn quantity_one() -> u32 {
83 1
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
89pub struct PaymentsSubscriptionUpsertOutput {
90 pub id: String,
92 pub status: String,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub current_period_end: Option<String>,
99}
100
101impl InternalVerb for PaymentsSubscriptionUpsert {
102 type Input = PaymentsSubscriptionUpsertInput;
103 type Output = PaymentsSubscriptionUpsertOutput;
104 const ID: &'static str = "payments.subscription.upsert";
105 const CATEGORY: VerbCategory = VerbCategory::Payments;
106}
107
108pub struct PaymentsWebhookVerify;
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
119#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
120pub struct PaymentsWebhookVerifyInput {
121 pub payload: String,
124 pub signature: String,
127 pub secret: String,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
134pub struct PaymentsWebhookVerifyOutput {
135 pub valid: bool,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub event_type: Option<String>,
142}
143
144impl InternalVerb for PaymentsWebhookVerify {
145 type Input = PaymentsWebhookVerifyInput;
146 type Output = PaymentsWebhookVerifyOutput;
147 const ID: &'static str = "payments.webhook.verify";
148 const CATEGORY: VerbCategory = VerbCategory::Payments;
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn verb_ids_match_canonical_namespace() {
157 for id in [
158 PaymentsChargeCreate::ID,
159 PaymentsSubscriptionUpsert::ID,
160 PaymentsWebhookVerify::ID,
161 ] {
162 assert!(id.starts_with("payments."), "{id}");
163 }
164 }
165
166 #[test]
167 fn verbs_are_under_payments_category() {
168 assert_eq!(PaymentsChargeCreate::CATEGORY, VerbCategory::Payments);
169 assert_eq!(PaymentsSubscriptionUpsert::CATEGORY, VerbCategory::Payments);
170 assert_eq!(PaymentsWebhookVerify::CATEGORY, VerbCategory::Payments);
171 }
172
173 #[test]
174 fn charge_create_optional_fields_omitted() {
175 let input = PaymentsChargeCreateInput {
176 amount_cents: 1000,
177 currency: "usd".into(),
178 description: None,
179 customer_id: None,
180 };
181 let wire = serde_json::to_value(&input).unwrap();
182 assert!(!wire.as_object().unwrap().contains_key("description"));
183 assert!(!wire.as_object().unwrap().contains_key("customer_id"));
184 }
185
186 #[test]
187 fn subscription_upsert_defaults_quantity_to_one() {
188 let wire = r#"{"customer_id":"cus_1","price_id":"price_1"}"#;
189 let parsed: PaymentsSubscriptionUpsertInput = serde_json::from_str(wire).unwrap();
190 assert_eq!(parsed.quantity, 1);
191 }
192
193 #[test]
194 fn webhook_verify_output_omits_event_type_on_failure() {
195 let out = PaymentsWebhookVerifyOutput {
196 valid: false,
197 event_type: None,
198 };
199 let wire = serde_json::to_value(&out).unwrap();
200 assert_eq!(wire["valid"], false);
201 assert!(!wire.as_object().unwrap().contains_key("event_type"));
202 }
203
204 #[test]
205 fn webhook_verify_output_includes_event_type_on_success() {
206 let out = PaymentsWebhookVerifyOutput {
207 valid: true,
208 event_type: Some("payment_intent.succeeded".into()),
209 };
210 let wire = serde_json::to_value(&out).unwrap();
211 assert_eq!(wire["valid"], true);
212 assert_eq!(wire["event_type"], "payment_intent.succeeded");
213 }
214
215 #[cfg(feature = "json-schema")]
216 #[test]
217 fn verbs_emit_schemas_via_for_verb() {
218 use super::super::VerbDescriptor;
219
220 let charge = VerbDescriptor::for_verb::<PaymentsChargeCreate>();
221 assert_eq!(charge.id, "payments.charge.create");
222 assert!(charge.input_schema.to_string().contains("amount_cents"));
223
224 let sub = VerbDescriptor::for_verb::<PaymentsSubscriptionUpsert>();
225 assert_eq!(sub.id, "payments.subscription.upsert");
226
227 let verify = VerbDescriptor::for_verb::<PaymentsWebhookVerify>();
228 assert_eq!(verify.id, "payments.webhook.verify");
229 assert!(verify.output_schema.to_string().contains("valid"));
230 }
231}