Skip to main content

allsource_core/application/use_cases/
process_payment.rs

1use crate::{
2    application::dto::{
3        BlockchainDto, ConfirmTransactionRequest, ConfirmTransactionResponse,
4        InitiatePaymentRequest, InitiatePaymentResponse, ListTransactionsResponse,
5        RefundTransactionRequest, RefundTransactionResponse, TransactionDto,
6    },
7    domain::{
8        entities::{AccessToken, Transaction},
9        repositories::{
10            AccessTokenRepository, ArticleRepository, CreatorRepository, TransactionRepository,
11        },
12        value_objects::{ArticleId, Money, TenantId, TransactionId, WalletAddress},
13    },
14    error::Result,
15};
16use sha2::{Digest, Sha256};
17use std::sync::Arc;
18
19/// Use Case: Initiate Payment
20///
21/// Initiates a payment transaction for article access.
22/// This creates a pending transaction that will be confirmed after on-chain verification.
23///
24/// Responsibilities:
25/// - Validate input (DTO validation)
26/// - Verify article exists and is purchasable
27/// - Verify creator can receive payments
28/// - Check for duplicate transaction signature (prevent replay attacks)
29/// - Create domain Transaction entity
30/// - Persist via repository
31/// - Return response DTO
32pub struct InitiatePaymentUseCase {
33    transaction_repository: Arc<dyn TransactionRepository>,
34    article_repository: Arc<dyn ArticleRepository>,
35    creator_repository: Arc<dyn CreatorRepository>,
36}
37
38impl InitiatePaymentUseCase {
39    pub fn new(
40        transaction_repository: Arc<dyn TransactionRepository>,
41        article_repository: Arc<dyn ArticleRepository>,
42        creator_repository: Arc<dyn CreatorRepository>,
43    ) -> Self {
44        Self {
45            transaction_repository,
46            article_repository,
47            creator_repository,
48        }
49    }
50
51    pub async fn execute(
52        &self,
53        request: InitiatePaymentRequest,
54    ) -> Result<InitiatePaymentResponse> {
55        // Check for duplicate signature (replay attack prevention)
56        if self
57            .transaction_repository
58            .signature_exists(&request.tx_signature)
59            .await?
60        {
61            return Err(crate::error::AllSourceError::ValidationError(
62                "Transaction signature already exists".to_string(),
63            ));
64        }
65
66        // Parse IDs
67        let tenant_id = TenantId::new(request.tenant_id)?;
68        let article_id = ArticleId::new(request.article_id)?;
69        let reader_wallet = WalletAddress::new(request.reader_wallet)?;
70
71        // Verify article exists and is purchasable
72        let article = self
73            .article_repository
74            .find_by_id(&article_id)
75            .await?
76            .ok_or_else(|| {
77                crate::error::AllSourceError::EntityNotFound("Article not found".to_string())
78            })?;
79
80        if !article.is_purchasable() {
81            return Err(crate::error::AllSourceError::ValidationError(
82                "Article is not available for purchase".to_string(),
83            ));
84        }
85
86        // Verify creator can receive payments
87        let creator = self
88            .creator_repository
89            .find_by_id(article.creator_id())
90            .await?
91            .ok_or_else(|| {
92                crate::error::AllSourceError::EntityNotFound("Creator not found".to_string())
93            })?;
94
95        creator.can_receive_payments()?;
96
97        // Determine blockchain
98        let blockchain = request.blockchain.unwrap_or(BlockchainDto::Solana).into();
99
100        // Create transaction
101        let transaction = Transaction::new(
102            tenant_id,
103            article_id,
104            *article.creator_id(),
105            reader_wallet,
106            Money::usd_cents(article.price_cents()),
107            creator.fee_percentage(),
108            blockchain,
109            request.tx_signature,
110        )?;
111
112        // Persist transaction
113        self.transaction_repository.save(&transaction).await?;
114
115        Ok(InitiatePaymentResponse {
116            transaction: TransactionDto::from(&transaction),
117        })
118    }
119}
120
121/// Use Case: Confirm Transaction
122///
123/// Confirms a transaction after on-chain verification.
124/// This creates an access token for the reader.
125pub struct ConfirmTransactionUseCase {
126    transaction_repository: Arc<dyn TransactionRepository>,
127    access_token_repository: Arc<dyn AccessTokenRepository>,
128    article_repository: Arc<dyn ArticleRepository>,
129    creator_repository: Arc<dyn CreatorRepository>,
130}
131
132impl ConfirmTransactionUseCase {
133    pub fn new(
134        transaction_repository: Arc<dyn TransactionRepository>,
135        access_token_repository: Arc<dyn AccessTokenRepository>,
136        article_repository: Arc<dyn ArticleRepository>,
137        creator_repository: Arc<dyn CreatorRepository>,
138    ) -> Self {
139        Self {
140            transaction_repository,
141            access_token_repository,
142            article_repository,
143            creator_repository,
144        }
145    }
146
147    pub async fn execute(
148        &self,
149        request: ConfirmTransactionRequest,
150    ) -> Result<ConfirmTransactionResponse> {
151        // Parse transaction ID
152        let transaction_id = TransactionId::parse(&request.transaction_id)?;
153
154        // Find transaction
155        let mut transaction = self
156            .transaction_repository
157            .find_by_id(&transaction_id)
158            .await?
159            .ok_or_else(|| {
160                crate::error::AllSourceError::EntityNotFound("Transaction not found".to_string())
161            })?;
162
163        // Confirm transaction
164        transaction.confirm()?;
165
166        // Save updated transaction
167        self.transaction_repository.save(&transaction).await?;
168
169        // Generate access token
170        let token_hash = generate_token_hash(&transaction);
171
172        let access_token = AccessToken::new_paid(
173            transaction.tenant_id().clone(),
174            transaction.article_id().clone(),
175            *transaction.creator_id(),
176            transaction.reader_wallet().clone(),
177            *transaction.id(),
178            token_hash.clone(),
179        )?;
180
181        // Save access token
182        self.access_token_repository.save(&access_token).await?;
183
184        // Update article stats
185        if let Some(mut article) = self
186            .article_repository
187            .find_by_id(transaction.article_id())
188            .await?
189        {
190            article.record_purchase(transaction.amount_cents());
191            self.article_repository.save(&article).await?;
192        }
193
194        // Update creator revenue
195        if let Some(mut creator) = self
196            .creator_repository
197            .find_by_id(transaction.creator_id())
198            .await?
199        {
200            creator.record_revenue(transaction.creator_amount_cents());
201            self.creator_repository.save(&creator).await?;
202        }
203
204        Ok(ConfirmTransactionResponse {
205            transaction: TransactionDto::from(&transaction),
206            access_token: Some(token_hash),
207        })
208    }
209}
210
211/// Use Case: Fail Transaction
212///
213/// Marks a transaction as failed after on-chain verification fails.
214pub struct FailTransactionUseCase;
215
216impl FailTransactionUseCase {
217    pub fn execute(mut transaction: Transaction, reason: &str) -> Result<TransactionDto> {
218        transaction.fail(reason)?;
219        Ok(TransactionDto::from(&transaction))
220    }
221}
222
223/// Use Case: Refund Transaction
224///
225/// Processes a refund for a confirmed transaction.
226/// This revokes the associated access token.
227pub struct RefundTransactionUseCase {
228    transaction_repository: Arc<dyn TransactionRepository>,
229    access_token_repository: Arc<dyn AccessTokenRepository>,
230}
231
232impl RefundTransactionUseCase {
233    pub fn new(
234        transaction_repository: Arc<dyn TransactionRepository>,
235        access_token_repository: Arc<dyn AccessTokenRepository>,
236    ) -> Self {
237        Self {
238            transaction_repository,
239            access_token_repository,
240        }
241    }
242
243    pub async fn execute(
244        &self,
245        request: RefundTransactionRequest,
246    ) -> Result<RefundTransactionResponse> {
247        // Parse transaction ID
248        let transaction_id = TransactionId::parse(&request.transaction_id)?;
249
250        // Find transaction
251        let mut transaction = self
252            .transaction_repository
253            .find_by_id(&transaction_id)
254            .await?
255            .ok_or_else(|| {
256                crate::error::AllSourceError::EntityNotFound("Transaction not found".to_string())
257            })?;
258
259        // Process refund
260        transaction.refund(&request.refund_tx_signature)?;
261
262        // Save updated transaction
263        self.transaction_repository.save(&transaction).await?;
264
265        // Revoke access token
266        let reason = request
267            .reason
268            .unwrap_or_else(|| "Refund processed".to_string());
269        self.access_token_repository
270            .revoke_by_transaction(&transaction_id, &reason)
271            .await?;
272
273        Ok(RefundTransactionResponse {
274            transaction: TransactionDto::from(&transaction),
275        })
276    }
277}
278
279/// Use Case: Dispute Transaction
280///
281/// Marks a transaction as disputed.
282pub struct DisputeTransactionUseCase;
283
284impl DisputeTransactionUseCase {
285    pub fn execute(mut transaction: Transaction, reason: &str) -> Result<TransactionDto> {
286        transaction.dispute(reason)?;
287        Ok(TransactionDto::from(&transaction))
288    }
289}
290
291/// Use Case: Resolve Dispute
292///
293/// Resolves a disputed transaction.
294pub struct ResolveDisputeUseCase;
295
296impl ResolveDisputeUseCase {
297    pub fn execute(mut transaction: Transaction, resolution: &str) -> Result<TransactionDto> {
298        transaction.resolve_dispute(resolution)?;
299        Ok(TransactionDto::from(&transaction))
300    }
301}
302
303/// Use Case: List Transactions
304///
305/// Returns a list of transactions.
306pub struct ListTransactionsUseCase;
307
308impl ListTransactionsUseCase {
309    pub fn execute(transactions: &[Transaction]) -> ListTransactionsResponse {
310        let transaction_dtos: Vec<TransactionDto> =
311            transactions.iter().map(TransactionDto::from).collect();
312        let count = transaction_dtos.len();
313
314        ListTransactionsResponse {
315            transactions: transaction_dtos,
316            count,
317        }
318    }
319}
320
321/// Generate a secure token hash from transaction data
322fn generate_token_hash(transaction: &Transaction) -> String {
323    let mut hasher = Sha256::new();
324    hasher.update(transaction.id().as_uuid().to_string().as_bytes());
325    hasher.update(transaction.tx_signature().as_bytes());
326    hasher.update(transaction.reader_wallet().to_string().as_bytes());
327    hasher.update(transaction.article_id().to_string().as_bytes());
328    format!("{:x}", hasher.finalize())
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334    use crate::domain::{
335        entities::{
336            AccessTokenId, Blockchain, Creator, CreatorStatus, PaywallArticle, TransactionStatus,
337        },
338        repositories::{
339            AccessTokenQuery, ArticleQuery, CreatorQuery, RevenueDataPoint, RevenueGranularity,
340            TransactionQuery,
341        },
342        value_objects::CreatorId,
343    };
344    use async_trait::async_trait;
345    use chrono::{DateTime, Utc};
346    use std::sync::Mutex;
347
348    const VALID_WALLET: &str = "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM";
349    const VALID_SIGNATURE: &str =
350        "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9g8eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9g";
351
352    struct MockTransactionRepository {
353        transactions: Mutex<Vec<Transaction>>,
354    }
355
356    impl MockTransactionRepository {
357        fn new() -> Self {
358            Self {
359                transactions: Mutex::new(Vec::new()),
360            }
361        }
362    }
363
364    #[async_trait]
365    impl TransactionRepository for MockTransactionRepository {
366        async fn save(&self, transaction: &Transaction) -> Result<()> {
367            let mut transactions = self.transactions.lock().unwrap();
368            if let Some(pos) = transactions.iter().position(|t| t.id() == transaction.id()) {
369                transactions[pos] = transaction.clone();
370            } else {
371                transactions.push(transaction.clone());
372            }
373            Ok(())
374        }
375
376        async fn find_by_id(&self, id: &TransactionId) -> Result<Option<Transaction>> {
377            let transactions = self.transactions.lock().unwrap();
378            Ok(transactions.iter().find(|t| t.id() == id).cloned())
379        }
380
381        async fn find_by_signature(&self, signature: &str) -> Result<Option<Transaction>> {
382            let transactions = self.transactions.lock().unwrap();
383            Ok(transactions
384                .iter()
385                .find(|t| t.tx_signature() == signature)
386                .cloned())
387        }
388
389        async fn find_by_article(
390            &self,
391            _article_id: &ArticleId,
392            _limit: usize,
393            _offset: usize,
394        ) -> Result<Vec<Transaction>> {
395            Ok(Vec::new())
396        }
397
398        async fn find_by_creator(
399            &self,
400            _creator_id: &CreatorId,
401            _limit: usize,
402            _offset: usize,
403        ) -> Result<Vec<Transaction>> {
404            Ok(Vec::new())
405        }
406
407        async fn find_by_reader(
408            &self,
409            _wallet: &WalletAddress,
410            _limit: usize,
411            _offset: usize,
412        ) -> Result<Vec<Transaction>> {
413            Ok(Vec::new())
414        }
415
416        async fn find_by_status(
417            &self,
418            _status: TransactionStatus,
419            _limit: usize,
420            _offset: usize,
421        ) -> Result<Vec<Transaction>> {
422            Ok(Vec::new())
423        }
424
425        async fn count(&self) -> Result<usize> {
426            Ok(self.transactions.lock().unwrap().len())
427        }
428
429        async fn count_by_status(&self, _status: TransactionStatus) -> Result<usize> {
430            Ok(0)
431        }
432
433        async fn get_creator_revenue(&self, _creator_id: &CreatorId) -> Result<u64> {
434            Ok(0)
435        }
436
437        async fn get_article_revenue(&self, _article_id: &ArticleId) -> Result<u64> {
438            Ok(0)
439        }
440
441        async fn query(&self, _query: &TransactionQuery) -> Result<Vec<Transaction>> {
442            Ok(Vec::new())
443        }
444
445        async fn get_revenue_by_period(
446            &self,
447            _creator_id: &CreatorId,
448            _start_date: DateTime<Utc>,
449            _end_date: DateTime<Utc>,
450            _granularity: RevenueGranularity,
451        ) -> Result<Vec<RevenueDataPoint>> {
452            Ok(Vec::new())
453        }
454    }
455
456    struct MockArticleRepository {
457        articles: Mutex<Vec<PaywallArticle>>,
458    }
459
460    impl MockArticleRepository {
461        fn new() -> Self {
462            Self {
463                articles: Mutex::new(Vec::new()),
464            }
465        }
466
467        fn add_article(&self, article: PaywallArticle) {
468            self.articles.lock().unwrap().push(article);
469        }
470    }
471
472    #[async_trait]
473    impl ArticleRepository for MockArticleRepository {
474        async fn save(&self, article: &PaywallArticle) -> Result<()> {
475            let mut articles = self.articles.lock().unwrap();
476            if let Some(pos) = articles.iter().position(|a| a.id() == article.id()) {
477                articles[pos] = article.clone();
478            } else {
479                articles.push(article.clone());
480            }
481            Ok(())
482        }
483
484        async fn find_by_id(&self, id: &ArticleId) -> Result<Option<PaywallArticle>> {
485            let articles = self.articles.lock().unwrap();
486            Ok(articles.iter().find(|a| a.id() == id).cloned())
487        }
488
489        async fn find_by_url(&self, _url: &str) -> Result<Option<PaywallArticle>> {
490            Ok(None)
491        }
492
493        async fn find_by_creator(
494            &self,
495            _creator_id: &CreatorId,
496            _limit: usize,
497            _offset: usize,
498        ) -> Result<Vec<PaywallArticle>> {
499            Ok(Vec::new())
500        }
501
502        async fn find_by_tenant(
503            &self,
504            _tenant_id: &TenantId,
505            _limit: usize,
506            _offset: usize,
507        ) -> Result<Vec<PaywallArticle>> {
508            Ok(Vec::new())
509        }
510
511        async fn find_active_by_creator(
512            &self,
513            _creator_id: &CreatorId,
514            _limit: usize,
515            _offset: usize,
516        ) -> Result<Vec<PaywallArticle>> {
517            Ok(Vec::new())
518        }
519
520        async fn find_by_status(
521            &self,
522            _status: crate::domain::entities::ArticleStatus,
523            _limit: usize,
524            _offset: usize,
525        ) -> Result<Vec<PaywallArticle>> {
526            Ok(Vec::new())
527        }
528
529        async fn count(&self) -> Result<usize> {
530            Ok(0)
531        }
532
533        async fn count_by_creator(&self, _creator_id: &CreatorId) -> Result<usize> {
534            Ok(0)
535        }
536
537        async fn count_by_status(
538            &self,
539            _status: crate::domain::entities::ArticleStatus,
540        ) -> Result<usize> {
541            Ok(0)
542        }
543
544        async fn delete(&self, _id: &ArticleId) -> Result<bool> {
545            Ok(false)
546        }
547
548        async fn query(&self, _query: &ArticleQuery) -> Result<Vec<PaywallArticle>> {
549            Ok(Vec::new())
550        }
551
552        async fn find_top_by_revenue(
553            &self,
554            _creator_id: Option<&CreatorId>,
555            _limit: usize,
556        ) -> Result<Vec<PaywallArticle>> {
557            Ok(Vec::new())
558        }
559
560        async fn find_recent(
561            &self,
562            _creator_id: Option<&CreatorId>,
563            _limit: usize,
564        ) -> Result<Vec<PaywallArticle>> {
565            Ok(Vec::new())
566        }
567    }
568
569    struct MockCreatorRepository {
570        creators: Mutex<Vec<Creator>>,
571    }
572
573    impl MockCreatorRepository {
574        fn new() -> Self {
575            Self {
576                creators: Mutex::new(Vec::new()),
577            }
578        }
579
580        fn add_creator(&self, creator: Creator) {
581            self.creators.lock().unwrap().push(creator);
582        }
583    }
584
585    #[async_trait]
586    impl CreatorRepository for MockCreatorRepository {
587        async fn create(&self, creator: Creator) -> Result<Creator> {
588            let mut creators = self.creators.lock().unwrap();
589            creators.push(creator.clone());
590            Ok(creator)
591        }
592
593        async fn save(&self, creator: &Creator) -> Result<()> {
594            let mut creators = self.creators.lock().unwrap();
595            if let Some(pos) = creators.iter().position(|c| c.id() == creator.id()) {
596                creators[pos] = creator.clone();
597            }
598            Ok(())
599        }
600
601        async fn find_by_id(&self, id: &CreatorId) -> Result<Option<Creator>> {
602            let creators = self.creators.lock().unwrap();
603            Ok(creators.iter().find(|c| c.id() == id).cloned())
604        }
605
606        async fn find_by_email(&self, _email: &str) -> Result<Option<Creator>> {
607            Ok(None)
608        }
609
610        async fn find_by_wallet(&self, _wallet: &WalletAddress) -> Result<Option<Creator>> {
611            Ok(None)
612        }
613
614        async fn find_by_tenant(
615            &self,
616            _tenant_id: &TenantId,
617            _limit: usize,
618            _offset: usize,
619        ) -> Result<Vec<Creator>> {
620            Ok(Vec::new())
621        }
622
623        async fn find_active(&self, _limit: usize, _offset: usize) -> Result<Vec<Creator>> {
624            Ok(Vec::new())
625        }
626
627        async fn count(&self) -> Result<usize> {
628            Ok(0)
629        }
630
631        async fn count_by_status(&self, _status: CreatorStatus) -> Result<usize> {
632            Ok(0)
633        }
634
635        async fn delete(&self, _id: &CreatorId) -> Result<bool> {
636            Ok(false)
637        }
638
639        async fn query(&self, _query: &CreatorQuery) -> Result<Vec<Creator>> {
640            Ok(Vec::new())
641        }
642    }
643
644    struct MockAccessTokenRepository {
645        tokens: Mutex<Vec<AccessToken>>,
646    }
647
648    impl MockAccessTokenRepository {
649        fn new() -> Self {
650            Self {
651                tokens: Mutex::new(Vec::new()),
652            }
653        }
654    }
655
656    #[async_trait]
657    impl AccessTokenRepository for MockAccessTokenRepository {
658        async fn save(&self, token: &AccessToken) -> Result<()> {
659            self.tokens.lock().unwrap().push(token.clone());
660            Ok(())
661        }
662
663        async fn find_by_id(&self, _id: &AccessTokenId) -> Result<Option<AccessToken>> {
664            Ok(None)
665        }
666
667        async fn find_by_hash(&self, _token_hash: &str) -> Result<Option<AccessToken>> {
668            Ok(None)
669        }
670
671        async fn find_by_transaction(
672            &self,
673            _transaction_id: &TransactionId,
674        ) -> Result<Option<AccessToken>> {
675            Ok(None)
676        }
677
678        async fn find_by_article_and_wallet(
679            &self,
680            _article_id: &ArticleId,
681            _wallet: &WalletAddress,
682        ) -> Result<Vec<AccessToken>> {
683            Ok(Vec::new())
684        }
685
686        async fn find_valid_token(
687            &self,
688            _article_id: &ArticleId,
689            _wallet: &WalletAddress,
690        ) -> Result<Option<AccessToken>> {
691            Ok(None)
692        }
693
694        async fn find_by_reader(
695            &self,
696            _wallet: &WalletAddress,
697            _limit: usize,
698            _offset: usize,
699        ) -> Result<Vec<AccessToken>> {
700            Ok(Vec::new())
701        }
702
703        async fn find_by_article(
704            &self,
705            _article_id: &ArticleId,
706            _limit: usize,
707            _offset: usize,
708        ) -> Result<Vec<AccessToken>> {
709            Ok(Vec::new())
710        }
711
712        async fn find_by_creator(
713            &self,
714            _creator_id: &CreatorId,
715            _limit: usize,
716            _offset: usize,
717        ) -> Result<Vec<AccessToken>> {
718            Ok(Vec::new())
719        }
720
721        async fn count(&self) -> Result<usize> {
722            Ok(0)
723        }
724
725        async fn count_valid(&self) -> Result<usize> {
726            Ok(0)
727        }
728
729        async fn count_by_article(&self, _article_id: &ArticleId) -> Result<usize> {
730            Ok(0)
731        }
732
733        async fn revoke(&self, _id: &AccessTokenId, _reason: &str) -> Result<bool> {
734            Ok(true)
735        }
736
737        async fn revoke_by_transaction(
738            &self,
739            _transaction_id: &TransactionId,
740            _reason: &str,
741        ) -> Result<usize> {
742            Ok(1)
743        }
744
745        async fn delete_expired(&self, _before: DateTime<Utc>) -> Result<usize> {
746            Ok(0)
747        }
748
749        async fn query(&self, _query: &AccessTokenQuery) -> Result<Vec<AccessToken>> {
750            Ok(Vec::new())
751        }
752    }
753
754    #[tokio::test]
755    async fn test_initiate_payment() {
756        let tx_repo = Arc::new(MockTransactionRepository::new());
757        let article_repo = Arc::new(MockArticleRepository::new());
758        let creator_repo = Arc::new(MockCreatorRepository::new());
759
760        // Create and add creator
761        let tenant_id = TenantId::new("test-tenant".to_string()).unwrap();
762        let wallet = WalletAddress::new(VALID_WALLET.to_string()).unwrap();
763        let mut creator = Creator::new(
764            tenant_id.clone(),
765            "creator@example.com".to_string(),
766            wallet,
767            None,
768        )
769        .unwrap();
770        creator.verify_email();
771        let creator_id = *creator.id();
772        creator_repo.add_creator(creator);
773
774        // Create and add article
775        let article_id = ArticleId::new("test-article".to_string()).unwrap();
776        let article = PaywallArticle::new(
777            article_id.clone(),
778            tenant_id.clone(),
779            creator_id,
780            "Test Article".to_string(),
781            "https://example.com/article".to_string(),
782            50,
783        )
784        .unwrap();
785        article_repo.add_article(article);
786
787        let use_case =
788            InitiatePaymentUseCase::new(tx_repo.clone(), article_repo.clone(), creator_repo);
789
790        let request = InitiatePaymentRequest {
791            tenant_id: "test-tenant".to_string(),
792            article_id: "test-article".to_string(),
793            reader_wallet: "11111111111111111111111111111111".to_string(),
794            tx_signature: VALID_SIGNATURE.to_string(),
795            blockchain: None,
796        };
797
798        let response = use_case.execute(request).await;
799        assert!(response.is_ok());
800
801        let response = response.unwrap();
802        assert_eq!(response.transaction.amount_cents, 50);
803        assert_eq!(
804            response.transaction.status,
805            crate::application::dto::TransactionStatusDto::Pending
806        );
807    }
808
809    #[test]
810    fn test_list_transactions() {
811        let tenant_id = TenantId::new("test-tenant".to_string()).unwrap();
812        let article_id = ArticleId::new("test-article".to_string()).unwrap();
813        let creator_id = CreatorId::new();
814        let wallet = WalletAddress::new(VALID_WALLET.to_string()).unwrap();
815
816        let transactions = vec![
817            Transaction::new(
818                tenant_id,
819                article_id,
820                creator_id,
821                wallet,
822                Money::usd_cents(50),
823                10,
824                Blockchain::Solana,
825                VALID_SIGNATURE.to_string(),
826            )
827            .unwrap(),
828        ];
829
830        let response = ListTransactionsUseCase::execute(&transactions);
831        assert_eq!(response.count, 1);
832        assert_eq!(response.transactions.len(), 1);
833    }
834}