| 103 |
103 |
|
pub pending_cents: i64,
|
| 104 |
104 |
|
}
|
| 105 |
105 |
|
|
|
106 |
+ |
/// Sum the entries whose currency is `want`, ignoring the rest.
|
|
107 |
+ |
///
|
|
108 |
+ |
/// Split out of `get_balance` so the filter has a test. A connected account's
|
|
109 |
+ |
/// Stripe balance carries one entry per currency it holds, and the sum comes
|
|
110 |
+ |
/// back as a bare `i64` with no currency attached to contradict it, so summing
|
|
111 |
+ |
/// the wrong entries reports another currency's money as this one's and looks
|
|
112 |
+ |
/// like a plausible number rather than an error.
|
|
113 |
+ |
fn sum_in_currency<'a, C>(entries: impl IntoIterator<Item = (&'a C, i64)>, want: &C) -> i64
|
|
114 |
+ |
where
|
|
115 |
+ |
C: PartialEq + 'a,
|
|
116 |
+ |
{
|
|
117 |
+ |
entries
|
|
118 |
+ |
.into_iter()
|
|
119 |
+ |
.filter(|(currency, _)| *currency == want)
|
|
120 |
+ |
.map(|(_, amount)| amount)
|
|
121 |
+ |
.sum()
|
|
122 |
+ |
}
|
|
123 |
+ |
|
| 106 |
124 |
|
/// Payment provider abstraction for checkout, connect, and webhook operations.
|
| 107 |
125 |
|
#[async_trait::async_trait]
|
| 108 |
126 |
|
pub trait PaymentProvider: Send + Sync {
|
| 296 |
314 |
|
) -> crate::error::Result<String>;
|
| 297 |
315 |
|
}
|
| 298 |
316 |
|
|
|
317 |
+ |
#[cfg(test)]
|
|
318 |
+ |
pub(crate) mod test_provider {
|
|
319 |
+ |
//! A crate-visible [`PaymentProvider`] double for lib tests.
|
|
320 |
+ |
//!
|
|
321 |
+ |
//! The integration suite already has `MockPaymentProvider`
|
|
322 |
+ |
//! (`tests/harness/stripe.rs`), which is richer: it captures checkout
|
|
323 |
+ |
//! sessions and signs webhooks. It lives in a separate test binary, so a
|
|
324 |
+ |
//! `--lib` test cannot reach it, and this is deliberately the smaller
|
|
325 |
+ |
//! thing. It answers the subscription-lifecycle calls and panics on
|
|
326 |
+ |
//! everything else, which is enough to test the code that fans those out
|
|
327 |
+ |
//! without a database, a router or a Stripe key.
|
|
328 |
+ |
//!
|
|
329 |
+ |
//! Implement a method here when a lib test needs it. Growing this toward
|
|
330 |
+ |
//! the harness's copy would give the crate two mocks to keep in agreement,
|
|
331 |
+ |
//! which is the imitation-oracle failure wiki `testing-posture` describes.
|
|
332 |
+ |
|
|
333 |
+ |
use std::collections::HashSet;
|
|
334 |
+ |
use std::sync::Mutex;
|
|
335 |
+ |
|
|
336 |
+ |
use super::*;
|
|
337 |
+ |
|
|
338 |
+ |
/// Records every subscription op it is asked for, and fails the ones whose
|
|
339 |
+ |
/// subscription id was listed as failing.
|
|
340 |
+ |
#[derive(Default)]
|
|
341 |
+ |
pub(crate) struct ScriptedProvider {
|
|
342 |
+ |
failing: HashSet<String>,
|
|
343 |
+ |
calls: Mutex<Vec<(&'static str, String)>>,
|
|
344 |
+ |
}
|
|
345 |
+ |
|
|
346 |
+ |
impl ScriptedProvider {
|
|
347 |
+ |
/// Every call succeeds.
|
|
348 |
+ |
pub(crate) fn healthy() -> Self {
|
|
349 |
+ |
Self::default()
|
|
350 |
+ |
}
|
|
351 |
+ |
|
|
352 |
+ |
/// Every call succeeds except those naming one of `sub_ids`.
|
|
353 |
+ |
pub(crate) fn failing(sub_ids: impl IntoIterator<Item = &'static str>) -> Self {
|
|
354 |
+ |
Self {
|
|
355 |
+ |
failing: sub_ids.into_iter().map(str::to_owned).collect(),
|
|
356 |
+ |
calls: Mutex::new(Vec::new()),
|
|
357 |
+ |
}
|
|
358 |
+ |
}
|
|
359 |
+ |
|
|
360 |
+ |
/// `(op, subscription id)` in the order they were applied.
|
|
361 |
+ |
pub(crate) fn calls(&self) -> Vec<(&'static str, String)> {
|
|
362 |
+ |
self.calls
|
|
363 |
+ |
.lock()
|
|
364 |
+ |
.expect("no test panics while holding this")
|
|
365 |
+ |
.clone()
|
|
366 |
+ |
}
|
|
367 |
+ |
|
|
368 |
+ |
fn record(&self, op: &'static str, sub_id: &str) -> crate::error::Result<()> {
|
|
369 |
+ |
self.calls
|
|
370 |
+ |
.lock()
|
|
371 |
+ |
.expect("no test panics while holding this")
|
|
372 |
+ |
.push((op, sub_id.to_string()));
|
|
373 |
+ |
if self.failing.contains(sub_id) {
|
|
374 |
+ |
return Err(crate::error::AppError::BadRequest(format!(
|
|
375 |
+ |
"scripted failure for {sub_id}"
|
|
376 |
+ |
)));
|
|
377 |
+ |
}
|
|
378 |
+ |
Ok(())
|
|
379 |
+ |
}
|
|
380 |
+ |
}
|
|
381 |
+ |
|
|
382 |
+ |
/// The methods no lib test drives yet. A call is a bug in the test, not a
|
|
383 |
+ |
/// condition to handle, so it panics rather than returning an error the
|
|
384 |
+ |
/// code under test would quietly count as a Stripe failure.
|
|
385 |
+ |
macro_rules! unused {
|
|
386 |
+ |
($($name:ident),+ $(,)?) => {
|
|
387 |
+ |
$(
|
|
388 |
+ |
#[allow(unused_variables)]
|
|
389 |
+ |
fn $name(&self) -> ! {
|
|
390 |
+ |
unimplemented!(
|
|
391 |
+ |
"ScriptedProvider::{} is not implemented; add it if a lib test needs it",
|
|
392 |
+ |
stringify!($name)
|
|
393 |
+ |
)
|
|
394 |
+ |
}
|
|
395 |
+ |
)+
|
|
396 |
+ |
};
|
|
397 |
+ |
}
|
|
398 |
+ |
|
|
399 |
+ |
impl ScriptedProvider {
|
|
400 |
+ |
unused!(
|
|
401 |
+ |
create_checkout_session,
|
|
402 |
+ |
create_guest_checkout_session,
|
|
403 |
+ |
create_subscription_checkout_session,
|
|
404 |
+ |
create_tip_checkout_session,
|
|
405 |
+ |
create_fan_plus_checkout_session,
|
|
406 |
+ |
create_creator_tier_checkout_session,
|
|
407 |
+ |
create_synckit_app_sub_checkout_session,
|
|
408 |
+ |
create_cart_checkout_session,
|
|
409 |
+ |
create_connect_account,
|
|
410 |
+ |
create_account_link,
|
|
411 |
+ |
fetch_account,
|
|
412 |
+ |
create_subscription_product_and_price,
|
|
413 |
+ |
get_balance,
|
|
414 |
+ |
cancel_platform_subscription,
|
|
415 |
+ |
set_platform_cancel_at_period_end,
|
|
416 |
+ |
create_billing_portal_session,
|
|
417 |
+ |
create_refund_for_transaction,
|
|
418 |
+ |
create_platform_credit_transfer,
|
|
419 |
+ |
create_platform_credit_reversal,
|
|
420 |
+ |
verify_webhook,
|
|
421 |
+ |
verify_webhook_v2,
|
|
422 |
+ |
create_synckit_customer,
|
|
423 |
+ |
create_synckit_subscription,
|
|
424 |
+ |
update_synckit_subscription_price,
|
|
425 |
+ |
update_synckit_app_sub_price,
|
|
426 |
+ |
cancel_synckit_subscription,
|
|
427 |
+ |
create_synckit_billing_portal,
|
|
428 |
+ |
);
|
|
429 |
+ |
}
|
|
430 |
+ |
|
|
431 |
+ |
#[async_trait::async_trait]
|
|
432 |
+ |
impl PaymentProvider for ScriptedProvider {
|
|
433 |
+ |
// ── what the fan-out drives ──
|
|
434 |
+ |
|
|
435 |
+ |
async fn pause_subscription(&self, sub: &str, _account: &str) -> crate::error::Result<()> {
|
|
436 |
+ |
self.record("pause", sub)
|
|
437 |
+ |
}
|
|
438 |
+ |
|
|
439 |
+ |
async fn resume_subscription(&self, sub: &str, _account: &str) -> crate::error::Result<()> {
|
|
440 |
+ |
self.record("resume", sub)
|
|
441 |
+ |
}
|
|
442 |
+ |
|
|
443 |
+ |
async fn cancel_subscription(&self, sub: &str, _account: &str) -> crate::error::Result<()> {
|
|
444 |
+ |
self.record("cancel", sub)
|
|
445 |
+ |
}
|
|
446 |
+ |
|
|
447 |
+ |
async fn set_cancel_at_period_end(
|
|
448 |
+ |
&self,
|
|
449 |
+ |
sub: &str,
|
|
450 |
+ |
_account: &str,
|
|
451 |
+ |
cancel: bool,
|
|
452 |
+ |
) -> crate::error::Result<()> {
|
|
453 |
+ |
self.record(
|
|
454 |
+ |
if cancel {
|
|
455 |
+ |
"set_cancel_at_period_end"
|
|
456 |
+ |
} else {
|
|
457 |
+ |
"clear_cancel_at_period_end"
|
|
458 |
+ |
},
|
|
459 |
+ |
sub,
|
|
460 |
+ |
)
|
|
461 |
+ |
}
|
|
462 |
+ |
|
|
463 |
+ |
// ── everything else ──
|
|
464 |
+ |
|
|
465 |
+ |
async fn create_checkout_session(
|
|
466 |
+ |
&self,
|
|
467 |
+ |
_params: &CheckoutParams<'_>,
|
|
468 |
+ |
) -> crate::error::Result<CheckoutResult> {
|
|
469 |
+ |
ScriptedProvider::create_checkout_session(self)
|
|
470 |
+ |
}
|
|
471 |
+ |
async fn create_guest_checkout_session(
|
|
472 |
+ |
&self,
|
|
473 |
+ |
_params: &GuestCheckoutParams<'_>,
|
|
474 |
+ |
) -> crate::error::Result<CheckoutResult> {
|
|
475 |
+ |
ScriptedProvider::create_guest_checkout_session(self)
|
|
476 |
+ |
}
|
|
477 |
+ |
async fn create_subscription_checkout_session(
|
|
478 |
+ |
&self,
|
|
479 |
+ |
_params: &SubscriptionCheckoutParams<'_>,
|
|
480 |
+ |
) -> crate::error::Result<CheckoutResult> {
|
|
481 |
+ |
ScriptedProvider::create_subscription_checkout_session(self)
|
|
482 |
+ |
}
|
|
483 |
+ |
async fn create_tip_checkout_session(
|
|
484 |
+ |
&self,
|
|
485 |
+ |
_params: &TipCheckoutParams<'_>,
|
|
486 |
+ |
) -> crate::error::Result<CheckoutResult> {
|
|
487 |
+ |
ScriptedProvider::create_tip_checkout_session(self)
|
|
488 |
+ |
}
|
|
489 |
+ |
async fn create_fan_plus_checkout_session(
|
|
490 |
+ |
&self,
|
|
491 |
+ |
_price_id: &str,
|
|
492 |
+ |
_user_id: crate::db::UserId,
|
|
493 |
+ |
_success_url: &str,
|
|
494 |
+ |
_cancel_url: &str,
|
|
495 |
+ |
) -> crate::error::Result<CheckoutResult> {
|
|
496 |
+ |
ScriptedProvider::create_fan_plus_checkout_session(self)
|
|
497 |
+ |
}
|
|
498 |
+ |
async fn create_creator_tier_checkout_session(
|
|
499 |
+ |
&self,
|
|
500 |
+ |
_price_id: &str,
|
|
501 |
+ |
_user_id: crate::db::UserId,
|
|
502 |
+ |
_tier: &str,
|
|
503 |
+ |
_success_url: &str,
|
|
504 |
+ |
_cancel_url: &str,
|
|
505 |
+ |
_trial_days: Option<i32>,
|
|
506 |
+ |
) -> crate::error::Result<CheckoutResult> {
|
|
507 |
+ |
ScriptedProvider::create_creator_tier_checkout_session(self)
|
|
508 |
+ |
}
|
|
509 |
+ |
async fn create_synckit_app_sub_checkout_session(
|
|
510 |
+ |
&self,
|
|
511 |
+ |
_params: &SynckitAppSubCheckoutParams<'_>,
|
|
512 |
+ |
) -> crate::error::Result<CheckoutResult> {
|
|
513 |
+ |
ScriptedProvider::create_synckit_app_sub_checkout_session(self)
|
|
514 |
+ |
}
|
|
515 |
+ |
async fn create_cart_checkout_session(
|
|
516 |
+ |
&self,
|
|
517 |
+ |
_params: &CartCheckoutParams<'_>,
|
|
518 |
+ |
) -> crate::error::Result<CheckoutResult> {
|
|
519 |
+ |
ScriptedProvider::create_cart_checkout_session(self)
|
|
520 |
+ |
}
|
|
521 |
+ |
async fn create_connect_account(
|
|
522 |
+ |
&self,
|
|
523 |
+ |
_email: &str,
|
|
524 |
+ |
) -> crate::error::Result<crate::db::StripeAccountId> {
|
|
525 |
+ |
ScriptedProvider::create_connect_account(self)
|
|
526 |
+ |
}
|
|
527 |
+ |
async fn create_account_link(
|
|
528 |
+ |
&self,
|
|
529 |
+ |
_account_id: &str,
|
|
530 |
+ |
_return_url: &str,
|
|
531 |
+ |
_refresh_url: &str,
|
|
532 |
+ |
) -> crate::error::Result<String> {
|
|
533 |
+ |
ScriptedProvider::create_account_link(self)
|
|
534 |
+ |
}
|
|
535 |
+ |
async fn fetch_account(&self, _account_id: &str) -> crate::error::Result<AccountUpdate> {
|
|
536 |
+ |
ScriptedProvider::fetch_account(self)
|
|
537 |
+ |
}
|
|
538 |
+ |
async fn create_subscription_product_and_price(
|
|
539 |
+ |
&self,
|
|
540 |
+ |
_connected_account_id: &str,
|
|
541 |
+ |
_tier_name: &str,
|
|
542 |
+ |
_tier_description: Option<&str>,
|
|
543 |
+ |
_price_cents: i64,
|
|
544 |
+ |
_currency: crate::currency::SettlementCurrency,
|
|
545 |
+ |
) -> crate::error::Result<(String, String)> {
|
|
546 |
+ |
ScriptedProvider::create_subscription_product_and_price(self)
|
|
547 |
+ |
}
|
|
548 |
+ |
async fn get_balance(
|
|
549 |
+ |
&self,
|
|
550 |
+ |
_account_id: &str,
|
|
551 |
+ |
_currency: crate::currency::SettlementCurrency,
|
|
552 |
+ |
) -> crate::error::Result<BalanceSummary> {
|
|
553 |
+ |
ScriptedProvider::get_balance(self)
|
|
554 |
+ |
}
|
|
555 |
+ |
async fn cancel_platform_subscription(&self, _sub: &str) -> crate::error::Result<()> {
|
|
556 |
+ |
ScriptedProvider::cancel_platform_subscription(self)
|
|
557 |
+ |
}
|
|
558 |
+ |
async fn set_platform_cancel_at_period_end(
|
|
559 |
+ |
&self,
|
|
560 |
+ |
_sub: &str,
|
|
561 |
+ |
_cancel: bool,
|
|
562 |
+ |
) -> crate::error::Result<()> {
|
|
563 |
+ |
ScriptedProvider::set_platform_cancel_at_period_end(self)
|
|
564 |
+ |
}
|
|
565 |
+ |
async fn create_billing_portal_session(
|
|
566 |
+ |
&self,
|
|
567 |
+ |
_customer_id: &str,
|
|
568 |
+ |
_return_url: &str,
|
|
569 |
+ |
) -> crate::error::Result<String> {
|
|
570 |
+ |
ScriptedProvider::create_billing_portal_session(self)
|
|
571 |
+ |
}
|
|
572 |
+ |
async fn create_refund_for_transaction(
|
|
573 |
+ |
&self,
|
|
574 |
+ |
_payment_intent_id: &str,
|
|
575 |
+ |
_connected_account_id: &str,
|
|
576 |
+ |
_amount_cents: i64,
|
|
577 |
+ |
_transaction_id: crate::db::TransactionId,
|
|
578 |
+ |
) -> crate::error::Result<()> {
|
|
579 |
+ |
ScriptedProvider::create_refund_for_transaction(self)
|
|
580 |
+ |
}
|
|
581 |
+ |
async fn create_platform_credit_transfer(
|
|
582 |
+ |
&self,
|
|
583 |
+ |
_connected_account_id: &str,
|
|
584 |
+ |
_amount_cents: i64,
|
|
585 |
+ |
_transaction_id: crate::db::TransactionId,
|
|
586 |
+ |
_currency: crate::currency::SettlementCurrency,
|
|
587 |
+ |
) -> crate::error::Result<String> {
|
|
588 |
+ |
ScriptedProvider::create_platform_credit_transfer(self)
|
|
589 |
+ |
}
|
|
590 |
+ |
async fn create_platform_credit_reversal(
|
|
591 |
+ |
&self,
|
|
592 |
+ |
_transfer_id: &str,
|
|
593 |
+ |
_amount_cents: i64,
|
|
594 |
+ |
_transaction_id: crate::db::TransactionId,
|
|
595 |
+ |
) -> crate::error::Result<()> {
|
|
596 |
+ |
ScriptedProvider::create_platform_credit_reversal(self)
|
|
597 |
+ |
}
|
|
598 |
+ |
fn verify_webhook(
|
|
599 |
+ |
&self,
|
|
600 |
+ |
_payload: &str,
|
|
601 |
+ |
_signature: &str,
|
|
602 |
+ |
) -> crate::error::Result<UntypedEvent> {
|
|
603 |
+ |
ScriptedProvider::verify_webhook(self)
|
|
604 |
+ |
}
|
|
605 |
+ |
fn verify_webhook_v2(
|
|
606 |
+ |
&self,
|
|
607 |
+ |
_payload: &str,
|
|
608 |
+ |
_signature: &str,
|
|
609 |
+ |
) -> crate::error::Result<serde_json::Value> {
|
|
610 |
+ |
ScriptedProvider::verify_webhook_v2(self)
|
|
611 |
+ |
}
|
|
612 |
+ |
async fn create_synckit_customer(
|
|
613 |
+ |
&self,
|
|
614 |
+ |
_developer_user_id: crate::db::UserId,
|
|
615 |
+ |
_app_id: crate::db::SyncAppId,
|
|
616 |
+ |
_email: &str,
|
|
617 |
+ |
_app_name: &str,
|
|
618 |
+ |
) -> crate::error::Result<String> {
|
|
619 |
+ |
ScriptedProvider::create_synckit_customer(self)
|
|
620 |
+ |
}
|
|
621 |
+ |
async fn create_synckit_subscription(
|
|
622 |
+ |
&self,
|
|
623 |
+ |
_customer_id: &str,
|
|
624 |
+ |
_app_id: crate::db::SyncAppId,
|
|
625 |
+ |
_app_name: &str,
|
|
626 |
+ |
_price_cents: i64,
|
|
627 |
+ |
) -> crate::error::Result<SynckitSubResult> {
|
|
628 |
+ |
ScriptedProvider::create_synckit_subscription(self)
|
|
629 |
+ |
}
|
|
630 |
+ |
async fn update_synckit_subscription_price(
|
|
631 |
+ |
&self,
|
|
632 |
+ |
_subscription_id: &str,
|
|
633 |
+ |
_new_price_cents: i64,
|
|
634 |
+ |
_app_name: &str,
|
|
635 |
+ |
) -> crate::error::Result<()> {
|
|
636 |
+ |
ScriptedProvider::update_synckit_subscription_price(self)
|
|
637 |
+ |
}
|
|
638 |
+ |
async fn update_synckit_app_sub_price(
|
|
639 |
+ |
&self,
|
|
640 |
+ |
_subscription_id: &str,
|
|
641 |
+ |
_new_price_cents: i64,
|
|
642 |
+ |
_interval: SyncBillingInterval,
|
|
643 |
+ |
_product_name: &str,
|
|
644 |
+ |
) -> crate::error::Result<()> {
|
|
645 |
+ |
ScriptedProvider::update_synckit_app_sub_price(self)
|
|
646 |
+ |
}
|
|
647 |
+ |
async fn cancel_synckit_subscription(&self, _sub: &str) -> crate::error::Result<()> {
|
|
648 |
+ |
ScriptedProvider::cancel_synckit_subscription(self)
|
|
649 |
+ |
}
|
|
650 |
+ |
async fn create_synckit_billing_portal(
|
|
651 |
+ |
&self,
|
|
652 |
+ |
_customer_id: &str,
|
|
653 |
+ |
_return_url: &str,
|
|
654 |
+ |
) -> crate::error::Result<String> {
|
|
655 |
+ |
ScriptedProvider::create_synckit_billing_portal(self)
|
|
656 |
+ |
}
|
|
657 |
+ |
}
|
|
658 |
+ |
}
|
|
659 |
+ |
|
| 299 |
660 |
|
#[async_trait::async_trait]
|
| 300 |
661 |
|
impl PaymentProvider for StripeClient {
|
| 301 |
662 |
|
async fn create_checkout_session(
|
| 456 |
817 |
|
) -> crate::error::Result<BalanceSummary> {
|
| 457 |
818 |
|
let balance = self.get_connected_account_balance(account_id).await?;
|
| 458 |
819 |
|
let want = currency.to_stripe();
|
| 459 |
|
- |
let available_cents: i64 = balance
|
| 460 |
|
- |
.available
|
| 461 |
|
- |
.iter()
|
| 462 |
|
- |
.filter(|b| b.currency == want)
|
| 463 |
|
- |
.map(|b| b.amount)
|
| 464 |
|
- |
.sum();
|
| 465 |
|
- |
let pending_cents: i64 = balance
|
| 466 |
|
- |
.pending
|
| 467 |
|
- |
.iter()
|
| 468 |
|
- |
.filter(|b| b.currency == want)
|
| 469 |
|
- |
.map(|b| b.amount)
|
| 470 |
|
- |
.sum();
|
|
820 |
+ |
let available_cents = sum_in_currency(
|
|
821 |
+ |
balance.available.iter().map(|b| (&b.currency, b.amount)),
|
|
822 |
+ |
&want,
|
|
823 |
+ |
);
|
|
824 |
+ |
let pending_cents = sum_in_currency(
|
|
825 |
+ |
balance.pending.iter().map(|b| (&b.currency, b.amount)),
|
|
826 |
+ |
&want,
|
|
827 |
+ |
);
|
| 471 |
828 |
|
Ok(BalanceSummary {
|
| 472 |
829 |
|
available_cents,
|
| 473 |
830 |
|
pending_cents,
|
| 685 |
1042 |
|
);
|
| 686 |
1043 |
|
}
|
| 687 |
1044 |
|
}
|
|
1045 |
+ |
|
|
1046 |
+ |
// ── sum_in_currency, the filter behind `get_balance` ──
|
|
1047 |
+ |
|
|
1048 |
+ |
use crate::currency::SettlementCurrency;
|
|
1049 |
+ |
|
|
1050 |
+ |
/// The entries a connected account holding three currencies would carry.
|
|
1051 |
+ |
fn mixed() -> Vec<(stripe_types::Currency, i64)> {
|
|
1052 |
+ |
vec![
|
|
1053 |
+ |
(SettlementCurrency::Usd.to_stripe(), 1_000),
|
|
1054 |
+ |
(SettlementCurrency::Gbp.to_stripe(), 2_500),
|
|
1055 |
+ |
(SettlementCurrency::Usd.to_stripe(), 250),
|
|
1056 |
+ |
(SettlementCurrency::Eur.to_stripe(), 9_999),
|
|
1057 |
+ |
]
|
|
1058 |
+ |
}
|
|
1059 |
+ |
|
|
1060 |
+ |
#[test]
|
|
1061 |
+ |
fn sums_every_entry_in_the_wanted_currency() {
|
|
1062 |
+ |
let entries = mixed();
|
|
1063 |
+ |
let total = sum_in_currency(
|
|
1064 |
+ |
entries.iter().map(|(c, a)| (c, *a)),
|
|
1065 |
+ |
&SettlementCurrency::Usd.to_stripe(),
|
|
1066 |
+ |
);
|
|
1067 |
+ |
assert_eq!(total, 1_250, "both USD entries, and only those");
|
|
1068 |
+ |
}
|
|
1069 |
+ |
|
|
1070 |
+ |
#[test]
|
|
1071 |
+ |
fn ignores_every_entry_in_another_currency() {
|
|
1072 |
+ |
let entries = mixed();
|
|
1073 |
+ |
for currency in SettlementCurrency::ALL {
|
|
1074 |
+ |
let total =
|
|
1075 |
+ |
sum_in_currency(entries.iter().map(|(c, a)| (c, *a)), ¤cy.to_stripe());
|
|
1076 |
+ |
let expected = match currency {
|
|
1077 |
+ |
SettlementCurrency::Usd => 1_250,
|
|
1078 |
+ |
SettlementCurrency::Gbp => 2_500,
|
|
1079 |
+ |
SettlementCurrency::Eur => 9_999,
|
|
1080 |
+ |
_ => 0,
|
|
1081 |
+ |
};
|
|
1082 |
+ |
assert_eq!(
|
|
1083 |
+ |
total, expected,
|
|
1084 |
+ |
"{currency} must see its own money and nobody else's"
|
|
1085 |
+ |
);
|
|
1086 |
+ |
}
|
|
1087 |
+ |
}
|
|
1088 |
+ |
|
|
1089 |
+ |
#[test]
|
|
1090 |
+ |
fn a_currency_the_account_does_not_hold_is_zero_rather_than_everything() {
|
|
1091 |
+ |
let entries = mixed();
|
|
1092 |
+ |
let total = sum_in_currency(
|
|
1093 |
+ |
entries.iter().map(|(c, a)| (c, *a)),
|
|
1094 |
+ |
&SettlementCurrency::Nzd.to_stripe(),
|
|
1095 |
+ |
);
|
|
1096 |
+ |
assert_eq!(
|
|
1097 |
+ |
total, 0,
|
|
1098 |
+ |
"an inverted filter would report 13,749 NZD cents the account never held"
|
|
1099 |
+ |
);
|
|
1100 |
+ |
}
|
| 688 |
1101 |
|
}
|