whmcs 0.1.1

Rust client for the WHMCS API
Documentation
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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
use serde::{Deserialize, Serialize};

use crate::models::{
    WhmcsSorting, deserialize_whmcs_bool, u32_id, users::UserId, whmcs_nested_vec,
};

u32_id!(ClientGroupId);
u32_id!(ClientId);

#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)]
/// Represents the lifecycle status of a client.
pub enum ClientStatus {
    /// The client is active.
    Active,
    /// The client is inactive.
    Inactive,
    /// The client is closed.
    Closed,
}

#[derive(Debug, Serialize, Clone, Copy)]
#[serde(rename_all = "lowercase")]
/// Represents the column to order by when retrieving clients.
pub enum ClientOrderBy {
    /// Ordering by client's unique ID number.
    Id,
    /// Ordering by client first name.
    FirstName,
    /// Ordering by client last name.
    LastName,
    /// Ordering by client company name.
    CompanyName,
    /// Ordering by client email address.
    Email,
    /// Ordering by client group ID.
    GroupId,
    /// Ordering by client creation date.
    DateCreated,
    /// Ordering by client status.
    Status,
}

#[derive(Debug, Serialize, Default)]
/// Parameters for filtering and sorting the clients on [`get_clients`](crate::WhmcsClient::get_clients).
pub struct GetClientParams {
    /// The offset for the returned log data (default: 0)
    #[serde(rename = "limitstart", skip_serializing_if = "Option::is_none")]
    pub limit_start: Option<u32>,
    /// The number of records to return (default: 25)
    #[serde(rename = "limitnum", skip_serializing_if = "Option::is_none")]
    pub limit_num: Option<u32>,
    /// The direction to sort the results. ASC or DESC. Default: ASC
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sorting: Option<WhmcsSorting>,
    /// Optional desired Client Status. ‘Active’, ‘Inactive’, or ‘Closed’.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<ClientStatus>,
    /// The search term to look for at the start of email, firstname, lastname, fullname or companyname
    #[serde(skip_serializing_if = "Option::is_none")]
    pub search: Option<String>,
    /// The column to order by. id, firstname, lastname, companyname, email, groupid, datecreated, status
    #[serde(rename = "orderby", skip_serializing_if = "Option::is_none")]
    pub order_by: Option<ClientOrderBy>,
}

impl GetClientParams {
    /// The offset for the returned log data (default: 0)
    #[must_use]
    pub const fn limit_start(mut self, limit_start: u32) -> Self {
        self.limit_start = Some(limit_start);
        self
    }

    /// The number of records to return (default: 25)
    #[must_use]
    pub const fn limit_num(mut self, limit_num: u32) -> Self {
        self.limit_num = Some(limit_num);
        self
    }

    /// The direction to sort the results. ASC or DESC. Default: ASC
    #[must_use]
    pub const fn sorting(mut self, sorting: WhmcsSorting) -> Self {
        self.sorting = Some(sorting);
        self
    }

    /// Optional desired Client Status. ‘Active’, ‘Inactive’, or ‘Closed’.
    #[must_use]
    pub const fn status(mut self, status: ClientStatus) -> Self {
        self.status = Some(status);
        self
    }

    /// The search term to look for at the start of email, firstname, lastname, fullname or companyname
    #[must_use]
    pub fn search(mut self, search: impl Into<String>) -> Self {
        self.search = Some(search.into());
        self
    }

    /// The column to order by. id, firstname, lastname, companyname, email, groupid, datecreated, status
    #[must_use]
    pub const fn order_by(mut self, order_by: ClientOrderBy) -> Self {
        self.order_by = Some(order_by);
        self
    }
}

#[derive(Debug, Serialize, Default)]
/// Parameters for obtaining the password for a client on [`get_client_password`](crate::WhmcsClient::get_client_password).
pub struct GetClientPasswordParams {
    /// The client ID to obtain the password for
    // WHMCS uses `userid` for the client ID, but we use `client_id` for consistency.
    #[serde(rename = "userid", skip_serializing_if = "Option::is_none")]
    pub client_id: Option<ClientId>,
    /// The email address to obtain the password for
    #[serde(skip_serializing_if = "Option::is_none")]
    pub email: Option<String>,
}

impl GetClientPasswordParams {
    /// The userid to obtain the password for
    #[must_use]
    pub fn client_id(mut self, client_id: impl Into<ClientId>) -> Self {
        self.client_id = Some(client_id.into());
        self
    }

    /// The email address to obtain the password for
    #[must_use]
    pub fn email(mut self, email: impl Into<String>) -> Self {
        self.email = Some(email.into());
        self
    }
}

#[derive(Debug, Serialize, Default)]
/// Parameters for obtaining the details for a client on [`get_client_details`](crate::WhmcsClient::get_client_details).
pub struct GetClientDetailsParams {
    /// The client id to obtain the details for. `client_id` or `email` is required
    #[serde(rename = "clientid", skip_serializing_if = "Option::is_none")]
    pub client_id: Option<ClientId>,
    /// The email address to obtain the details for. `client_id` or `email` is required
    #[serde(skip_serializing_if = "Option::is_none")]
    pub email: Option<String>,
    /// Also return additional client statistics.
    #[serde(rename = "stats", skip_serializing_if = "Option::is_none")]
    pub include_stats: Option<bool>,
}

impl GetClientDetailsParams {
    /// The client id to obtain the details for. `client_id` or `email` is required
    #[must_use]
    pub fn client_id(mut self, client_id: impl Into<ClientId>) -> Self {
        self.client_id = Some(client_id.into());
        self
    }

    /// The email address to obtain the details for. `client_id` or `email` is required
    #[must_use]
    pub fn email(mut self, email: impl Into<String>) -> Self {
        self.email = Some(email.into());
        self
    }

    /// Also return additional client statistics.
    #[must_use]
    pub const fn include_stats(mut self, include_stats: bool) -> Self {
        self.include_stats = Some(include_stats);
        self
    }
}

#[derive(Debug, Deserialize, Clone)]
/// Represents a client group in WHMCS. Returned by [`get_client_groups`](crate::WhmcsClient::get_client_groups).
pub struct ClientGroup {
    /// The ID number of the client group.
    pub id: ClientGroupId,
    /// The name of the client group.
    #[serde(rename = "groupname")]
    pub name: String,
    /// The color of the client group.
    #[serde(rename = "groupcolor")]
    pub color: u8,
    /// The discount percentage for the client group.
    #[serde(rename = "discountpercent")]
    pub discount_percentage: f32,
    /// Whether the client group is exempt from suspension term.
    #[serde(rename = "susptermexempt")]
    #[serde(deserialize_with = "deserialize_whmcs_bool")]
    pub suspension_term_exempt: bool,
    /// Whether the client group should have separate invoices.
    #[serde(rename = "separateinvoices")]
    #[serde(deserialize_with = "deserialize_whmcs_bool")]
    pub separate_invoices: bool,
}

#[derive(Debug, Deserialize, Clone)]
#[non_exhaustive]
/// Represents a client in WHMCS. Returned by [`get_clients`](crate::WhmcsClient::get_clients).
pub struct Client {
    /// A client's unique ID number.
    pub id: ClientId,
    /// A client's first name.
    #[serde(rename = "firstname")]
    pub first_name: String,
    /// A client's last name.
    #[serde(rename = "lastname")]
    pub last_name: String,
    /// A client's email address.
    pub email: String,
    /// The name of the company employing a client.
    #[serde(rename = "companyname")]
    pub company_name: String,
    /// Initial creation of the client data.
    ///
    /// Note: If the user was created before WHMCS 6.0.0, this will be set to 0000-00-00 00:00:00
    #[serde(rename = "datecreated")]
    pub date_created: String,
    /// The ID number of the group that a client belongs to.
    #[serde(rename = "groupid")]
    pub group_id: ClientGroupId,
    /// A client's status, either [`ClientStatus::Active`], [`ClientStatus::Inactive`], or [`ClientStatus::Closed`].
    #[serde(rename = "status")]
    pub status: ClientStatus,
}

#[derive(Debug, Deserialize, Clone)]
#[non_exhaustive]
#[allow(clippy::struct_excessive_bools)]
/// Represents a client's email preferences. Returned by [`get_client_details`](crate::WhmcsClient::get_client_details).
pub struct EmailPreference {
    /// Whether the client wants to receive general emails.
    #[serde(deserialize_with = "deserialize_whmcs_bool")]
    pub general: bool,
    /// Whether the client wants to receive invoice emails.
    #[serde(deserialize_with = "deserialize_whmcs_bool")]
    pub invoice: bool,
    /// Whether the client wants to receive support emails.
    #[serde(deserialize_with = "deserialize_whmcs_bool")]
    pub support: bool,
    /// Whether the client wants to receive product emails.
    #[serde(deserialize_with = "deserialize_whmcs_bool")]
    pub product: bool,
    /// Whether the client wants to receive domain emails.
    #[serde(deserialize_with = "deserialize_whmcs_bool")]
    pub domain: bool,
    /// Whether the client wants to receive affiliate emails.
    #[serde(deserialize_with = "deserialize_whmcs_bool")]
    pub affiliate: bool,
}

#[derive(Debug, Deserialize, Clone)]
#[non_exhaustive]
/// Represents custom fields on a client.
pub struct CustomField {
    /// The ID of the custom field.
    // TODO: Replace with custom field ID
    pub id: u32,
    /// The value of the custom field.
    pub value: String,
}

#[derive(Debug, Deserialize, Clone)]
#[non_exhaustive]
/// Represents a short data representation of a user belonging to a client.
/// Returned by [`get_client_details`](crate::WhmcsClient::get_client_details).
pub struct ClientUser {
    /// The ID of the user.
    pub id: UserId,
    /// The name of the user.
    pub name: String,
    /// The email address of the user.
    pub email: String,
    /// Whether the user is the owner of the client.
    #[serde(deserialize_with = "deserialize_whmcs_bool")]
    pub is_owner: bool,
}

#[derive(Debug, Deserialize, Clone)]
#[non_exhaustive]
/// Represents a client's statistics. Returned by [`get_client_details`](crate::WhmcsClient::get_client_details)
/// when `include_stats` is set to `true`.
pub struct ClientStats {
    /// The number of due invoices.
    #[serde(rename = "numdueinvoices")]
    pub due_invoices: u32,
    /// The balance of due invoices.
    #[serde(rename = "dueinvoicesbalance")]
    pub due_invoices_balance: String,
    /// Whether the client is in credit.
    #[serde(rename = "incredit", deserialize_with = "deserialize_whmcs_bool")]
    pub in_credit: bool,
    /// The credit balance of the client.
    #[serde(rename = "creditbalance")]
    pub credit_balance: String,
    /// The gross revenue of the client.
    #[serde(rename = "grossRevenue")]
    pub gross_revenue: String,
    /// The expenses of the client.
    pub expenses: String,
    /// The income of the client.
    pub income: String,
    /// The number of overdue invoices.
    #[serde(rename = "numoverdueinvoices")]
    pub overdue_invoices: u32,
    /// The balance of overdue invoices.
    #[serde(rename = "overdueinvoicesbalance")]
    pub overdue_invoices_balance: String,
    /// The number of draft invoices.
    #[serde(rename = "numDraftInvoices")]
    pub draft_invoices: u32,
    /// The balance of draft invoices.
    #[serde(rename = "draftInvoicesBalance")]
    pub draft_invoices_balance: String,
    /// The number of unpaid invoices.
    #[serde(rename = "numunpaidinvoices")]
    pub unpaid_invoices: u32,
    /// The balance of unpaid invoices.
    #[serde(rename = "unpaidinvoicesamount")]
    pub unpaid_invoices_balance: String,
    /// The number of paid invoices.
    #[serde(rename = "numpaidinvoices")]
    pub paid_invoices: u32,
    /// The balance of paid invoices.
    #[serde(rename = "paidinvoicesamount")]
    pub paid_invoices_balance: String,
    /// The number of cancelled invoices.
    #[serde(rename = "numcancelledinvoices")]
    pub cancelled_invoices: u32,
    /// The balance of cancelled invoices.
    #[serde(rename = "cancelledinvoicesamount")]
    pub cancelled_invoices_balance: String,
    /// The number of refunded invoices.
    #[serde(rename = "numrefundedinvoices")]
    pub refunded_invoices: u32,
    /// The balance of refunded invoices.
    #[serde(rename = "refundedinvoicesamount")]
    pub refunded_invoices_balance: String,
    /// The number of collections invoices.
    #[serde(rename = "numcollectionsinvoices")]
    pub collections_invoices: u32,
    /// The balance of collections invoices.
    #[serde(rename = "collectionsinvoicesamount")]
    pub collections_invoices_balance: String,
    /// The number of payment pending invoices.
    #[serde(rename = "numpaymentpendinginvoices")]
    pub payment_pending_invoices: u32,
    /// The balance of payment pending invoices.
    #[serde(rename = "paymentpendinginvoicesamount")]
    pub payment_pending_invoices_balance: String,
    /// The number of active hosting products.
    #[serde(rename = "productsnumactivehosting")]
    pub active_hosting_products: u32,
    /// The number of hosting products.
    #[serde(rename = "productsnumhosting")]
    pub hosting_products: u32,
    /// The number of active reseller products.
    #[serde(rename = "productsnumactivereseller")]
    pub active_reseller_products: u32,
    /// The number of reseller products.
    #[serde(rename = "productsnumreseller")]
    pub reseller_products: u32,
    /// The number of active servers.
    #[serde(rename = "productsnumactiveservers")]
    pub active_servers: u32,
    /// The number of servers.
    #[serde(rename = "productsnumservers")]
    pub servers: u32,
    /// The number of active other products.
    #[serde(rename = "productsnumactiveother")]
    pub active_other_products: u32,
    /// The number of other products.
    #[serde(rename = "productsnumother")]
    pub other_products: u32,
    /// The number of active products.
    #[serde(rename = "productsnumactive")]
    pub active_products: u32,
    /// The number of total products.
    #[serde(rename = "productsnumtotal")]
    pub total_products: u32,
    /// The number of active domains.
    #[serde(rename = "numactivedomains")]
    pub active_domains: u32,
    /// The number of domains.
    #[serde(rename = "numdomains")]
    pub domains: u32,
    /// The number of accepted quotes.
    #[serde(rename = "numacceptedquotes")]
    pub accepted_quotes: u32,
    /// The number of quotes.
    #[serde(rename = "numquotes")]
    pub quotes: u32,
    /// The number of tickets.
    #[serde(rename = "numtickets")]
    pub tickets: u32,
    /// The number of active tickets.
    #[serde(rename = "numactivetickets")]
    pub active_tickets: u32,
    /// The number of affiliate signups.
    #[serde(rename = "numaffiliatesignups")]
    pub affiliate_signups: u32,
    /// Whether the client is an affiliate.
    #[serde(rename = "isAffiliate", deserialize_with = "deserialize_whmcs_bool")]
    pub is_affiliate: bool,
}

#[derive(Debug, Deserialize, Clone)]
#[non_exhaustive]
#[allow(clippy::struct_excessive_bools)]
/// Represents the details of a client. Returned by [`get_client_details`](crate::WhmcsClient::get_client_details).
pub struct ClientDetails {
    /// The ID of the client.
    pub client_id: ClientId,
    /// The ID of the owner user.
    pub owner_user_id: UserId,
    /// The ID of the user.
    #[serde(rename = "userid")]
    pub user_id: UserId,
    /// The UUID of the client.
    pub uuid: String,
    /// The first name of the client.
    #[serde(rename = "firstname")]
    pub first_name: String,
    /// The last name of the client.
    #[serde(rename = "lastname")]
    pub last_name: String,
    /// The full name of the client.
    #[serde(rename = "fullname")]
    pub full_name: String,
    /// The name of the company employing the client.
    #[serde(rename = "companyname")]
    pub company_name: String,
    /// The email address of the client.
    pub email: String,
    /// The first line of the client's address.
    pub address1: String,
    /// The second line of the client's address.
    pub address2: String,
    /// The city of the client's address.
    pub city: String,
    /// The full state of the client's address.
    #[serde(rename = "fullstate")]
    pub full_state: String,
    /// The state of the client's address.
    pub state: String,
    /// The postcode of the client's address.
    pub postcode: String,
    /// The country code of the client's address.
    #[serde(rename = "countrycode")]
    pub country_code: String,
    /// The country of the client's address.
    pub country: String,
    /// The phone number of the client.
    #[serde(rename = "phonenumber")]
    pub phone_number: String,
    /// The tax ID of the client.
    pub tax_id: String,
    /// The email preferences of the client.
    pub email_preferences: EmailPreference,
    /// The state code of the client's address.
    #[serde(rename = "statecode")]
    pub state_code: String,
    /// The country name of the client's address.
    #[serde(rename = "countryname")]
    pub country_name: String,
    /// The country code of the client's phone number.
    #[serde(rename = "phonecc")]
    pub phone_country_code: u32,
    /// The formatted phone number of the client.
    #[serde(rename = "phonenumberformatted")]
    pub phone_number_formatted: String,
    /// The telephone number of the client.
    #[serde(rename = "telephoneNumber")]
    pub telephone_number: String,
    /// The billing client ID of the client.
    #[serde(rename = "billingcid")]
    pub billing_cid: ClientId,
    /// The notes of the client.
    pub notes: String,
    /// The currency of the client.
    // TODO: Replace with currency ID
    pub currency: u8,
    /// The default gateway of the client.
    #[serde(rename = "defaultgateway")]
    pub default_gateway: String,
    /// The group ID of the client.
    #[serde(rename = "groupid")]
    pub group_id: ClientGroupId,
    /// The status of the client.
    pub status: ClientStatus,
    /// The credit of the client.
    pub credit: String,
    /// Whether the client is tax exempt.
    #[serde(rename = "taxexempt", deserialize_with = "deserialize_whmcs_bool")]
    pub tax_exempt: bool,
    /// Whether the client has an override for late fees.
    #[serde(rename = "latefeeoveride", deserialize_with = "deserialize_whmcs_bool")]
    pub late_fee_override: bool,
    /// Whether the client has an override for due notices.
    #[serde(
        rename = "overideduenotices",
        deserialize_with = "deserialize_whmcs_bool"
    )]
    pub override_due_notices: bool,
    /// Whether the client has separate invoices.
    #[serde(
        rename = "separateinvoices",
        deserialize_with = "deserialize_whmcs_bool"
    )]
    pub separate_invoices: bool,
    /// Whether the client has disabled auto CC.
    #[serde(rename = "disableautocc", deserialize_with = "deserialize_whmcs_bool")]
    pub disable_auto_cc: bool,
    /// Whether the client has opted out of email.
    #[serde(rename = "emailoptout", deserialize_with = "deserialize_whmcs_bool")]
    pub email_optout: bool,
    /// Whether the client has opted in to marketing emails.
    #[serde(
        rename = "marketing_emails_opt_in",
        deserialize_with = "deserialize_whmcs_bool"
    )]
    pub marketing_emails_opt_in: bool,
    /// Whether the client has an override for auto close.
    #[serde(
        rename = "overrideautoclose",
        deserialize_with = "deserialize_whmcs_bool"
    )]
    pub override_auto_close: bool,
    /// Whether the client allows single sign on.
    #[serde(
        rename = "allowSingleSignOn",
        deserialize_with = "deserialize_whmcs_bool"
    )]
    pub allow_single_sign_on: bool,
    /// Whether the client's email address has been verified.
    #[serde(deserialize_with = "deserialize_whmcs_bool")]
    pub email_verified: bool,
    /// The language of the client.
    pub language: String,
    /// Whether the client is opted in to marketing emails.
    #[serde(
        rename = "isOptedInToMarketingEmails",
        deserialize_with = "deserialize_whmcs_bool"
    )]
    pub is_opted_in_to_marketing_emails: bool,
    /// The last login of the client.
    #[serde(rename = "lastlogin")]
    pub last_login: String,
    /// The currency code of the client.
    pub currency_code: String,
    /// The custom fields of the client.
    #[serde(rename = "customfields")]
    pub custom_fields: Option<Vec<CustomField>>,
    /// The users of the client.
    #[serde(deserialize_with = "deserialize_client_users_nested")]
    pub users: Vec<ClientUser>,
}

whmcs_nested_vec!(deserialize_client_groups_nested, groups, ClientGroup);
whmcs_nested_vec!(deserialize_clients_nested, client, Client);
whmcs_nested_vec!(deserialize_client_users_nested, user, ClientUser);

#[derive(Debug, Deserialize)]
#[non_exhaustive]
/// Response from [`get_client_groups`](crate::WhmcsClient::get_client_groups).
pub struct GetClientGroupsResponse {
    /// The total number of results available
    #[serde(rename = "totalresults")]
    pub total_results: u32,
    /// The client group entries returned
    #[serde(default)]
    #[serde(
        rename = "groups",
        deserialize_with = "deserialize_client_groups_nested"
    )]
    pub groups: Vec<ClientGroup>,
}

#[derive(Debug, Deserialize)]
#[non_exhaustive]
/// Response from [`get_client_password`](crate::WhmcsClient::get_client_password).
pub struct GetClientPasswordResponse {
    /// The encrypted password for the client
    pub password: String,
}

#[derive(Debug, Deserialize)]
#[non_exhaustive]
/// Response from [`get_clients`](crate::WhmcsClient::get_clients).
pub struct GetClientsResponse {
    /// The total number of results available
    #[serde(rename = "totalresults")]
    pub total_results: u32,
    /// The starting number for the returned results
    #[serde(rename = "startnumber")]
    pub start_number: u32,
    /// The number of results returned
    #[serde(rename = "numreturned")]
    pub number_returned: u32,
    /// The client entries returned
    #[serde(default)]
    #[serde(rename = "clients", deserialize_with = "deserialize_clients_nested")]
    pub clients: Vec<Client>,
}

#[derive(Debug, Deserialize)]
#[non_exhaustive]
/// Response from [`get_client_details`](crate::WhmcsClient::get_client_details).
pub struct GetClientDetailsResponse {
    /// The information on the client
    pub client: ClientDetails,
    /// Additional statistics for the client returned
    pub stats: Option<ClientStats>,
}

#[cfg(test)]
mod clients_model_tests {
    use super::*;

    #[test]
    fn get_clients_response_deserializes_nested_clients() {
        let json = r#"{
            "totalresults": 2,
            "startnumber": 0,
            "numreturned": 2,
            "clients": {
                "client": [
                    {
                        "id": 1,
                        "firstname": "A",
                        "lastname": "B",
                        "email": "a@b.c",
                        "companyname": "",
                        "datecreated": "2020-01-01",
                        "groupid": 0,
                        "status": "Active"
                    },
                    {
                        "id": 2,
                        "firstname": "C",
                        "lastname": "D",
                        "email": "c@d.e",
                        "companyname": "Co",
                        "datecreated": "2020-01-02",
                        "groupid": 1,
                        "status": "Inactive"
                    }
                ]
            }
        }"#;
        let r: GetClientsResponse = serde_json::from_str(json).unwrap();
        assert_eq!(r.total_results, 2);
        assert_eq!(r.number_returned, 2);
        assert_eq!(r.clients.len(), 2);
        assert_eq!(r.clients[0].email, "a@b.c");
        assert_eq!(r.clients[1].status, ClientStatus::Inactive);
    }

    #[test]
    fn get_client_params_serializes_expected_keys() {
        let p = GetClientParams::default()
            .search("needle")
            .limit_start(10)
            .limit_num(5)
            .sorting(crate::models::WhmcsSorting::Descending)
            .status(ClientStatus::Active)
            .order_by(ClientOrderBy::Email);
        let v = serde_json::to_value(&p).unwrap();
        assert_eq!(v["search"], "needle");
        assert_eq!(v["limitstart"], 10);
        assert_eq!(v["limitnum"], 5);
        assert_eq!(v["sorting"], "DESC");
        assert_eq!(v["status"], "Active");
        assert_eq!(v["orderby"], "email");
    }

    #[test]
    fn get_client_details_params_serializes_clientid_and_stats() {
        let p = GetClientDetailsParams::default()
            .client_id(ClientId::new(42))
            .include_stats(true);
        let v = serde_json::to_value(&p).unwrap();
        assert_eq!(v["clientid"], 42);
        assert_eq!(v["stats"], true);
        assert!(v.get("email").is_none());
    }

    #[test]
    fn client_id_new_and_from_u32() {
        assert_eq!(ClientId::from(7_u32), ClientId::new(7));
    }
}