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
15pub 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 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 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 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 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 let blockchain = request.blockchain.unwrap_or(BlockchainDto::Solana).into();
95
96 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 self.transaction_repository.save(&transaction).await?;
110
111 Ok(InitiatePaymentResponse {
112 transaction: TransactionDto::from(&transaction),
113 })
114 }
115}
116
117pub 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 let transaction_id = TransactionId::parse(&request.transaction_id)?;
149
150 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 transaction.confirm()?;
161
162 self.transaction_repository.save(&transaction).await?;
164
165 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 self.access_token_repository.save(&access_token).await?;
179
180 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 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
207pub 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
219pub 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 let transaction_id = TransactionId::parse(&request.transaction_id)?;
245
246 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 transaction.refund(&request.refund_tx_signature)?;
257
258 self.transaction_repository.save(&transaction).await?;
260
261 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
275pub 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
287pub 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
299pub 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
317fn 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 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 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}