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
19pub 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 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 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 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 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 let blockchain = request.blockchain.unwrap_or(BlockchainDto::Solana).into();
99
100 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 self.transaction_repository.save(&transaction).await?;
114
115 Ok(InitiatePaymentResponse {
116 transaction: TransactionDto::from(&transaction),
117 })
118 }
119}
120
121pub 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 let transaction_id = TransactionId::parse(&request.transaction_id)?;
153
154 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 transaction.confirm()?;
165
166 self.transaction_repository.save(&transaction).await?;
168
169 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 self.access_token_repository.save(&access_token).await?;
183
184 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 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
211pub 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
223pub 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 let transaction_id = TransactionId::parse(&request.transaction_id)?;
249
250 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 transaction.refund(&request.refund_tx_signature)?;
261
262 self.transaction_repository.save(&transaction).await?;
264
265 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
279pub 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
291pub 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
303pub 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
321fn 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 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 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}