Skip to main content

allsource_core/application/use_cases/
process_payment.rs

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