Skip to content

Sistema de Wallet e Tokens - Sport Tech Club

Índice


Visão Geral

Objetivo

Criar um sistema de economia virtual própria da arena esportiva, permitindo:

  • Valorização da participação e engajamento
  • Gamificação e recompensas
  • Apostas não-financeiras entre times
  • Premiações em campeonatos
  • Cashback por consumo
  • Ecossistema fechado e auditável

Princípios Fundamentais

┌─────────────────────────────────────────────────────────────┐
│  VALOR ≠ DINHEIRO                                            │
├─────────────────────────────────────────────────────────────┤
│  • ArenaCoin é uma ABSTRAÇÃO DE VALOR                       │
│  • Não é moeda regulada (não é criptomoeda)                 │
│  • Sistema fechado (arena ecosystem)                        │
│  • Conversibilidade controlada pela arena                   │
│  • Foco em ENGAJAMENTO, não especulação                     │
└─────────────────────────────────────────────────────────────┘

1. Auditabilidade Total

  • Toda transação é registrada em ledger imutável
  • Rastreabilidade completa de origem e destino
  • Histórico perpétuo para compliance

2. Separação de Concerns

  • Wallet (armazenamento) ≠ Transaction (movimentação)
  • Token Definition (regras) ≠ Token Balance (saldo)
  • Business Rules (negócio) ≠ Technical Rules (técnica)

3. Integrabilidade

  • Parte do Antóctica Ecosystem
  • Outros apps podem emitir/aceitar ArenaCoin
  • ApplicationWallet para integração cross-platform

4. Consistência Eventual

  • Event-driven para escalabilidade
  • CQRS para leitura/escrita
  • Saga pattern para transações distribuídas

Arquitetura

Visão Macro

┌─────────────────────────────────────────────────────────────────────┐
│                      SPORT TECH CLUB ECOSYSTEM                       │
├─────────────────────────────────────────────────────────────────────┤
│                                                                       │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐          │
│  │  User Wallet │    │ Arena Wallet │    │ Event Wallet │          │
│  │              │    │              │    │              │          │
│  │  • Players   │    │  • Cashback  │    │  • Tournament│          │
│  │  • Students  │    │  • Prizes    │    │  • Bets      │          │
│  │  • Coaches   │    │  • Sponsors  │    │  • Temp Pool │          │
│  └──────────────┘    └──────────────┘    └──────────────┘          │
│         │                    │                    │                  │
│         └────────────────────┴────────────────────┘                  │
│                              │                                       │
│                    ┌─────────▼─────────┐                            │
│                    │  TRANSACTION      │                            │
│                    │     ENGINE        │                            │
│                    │                   │                            │
│                    │  • Credit/Debit   │                            │
│                    │  • Transfer       │                            │
│                    │  • Bet/Reward     │                            │
│                    │  • Cashback       │                            │
│                    └─────────┬─────────┘                            │
│                              │                                       │
│                    ┌─────────▼─────────┐                            │
│                    │   LEDGER STORE    │                            │
│                    │  (Event Sourcing) │                            │
│                    └───────────────────┘                            │
│                                                                       │
└─────────────────────────────────────────────────────────────────────┘
         │                              │                              
         ▼                              ▼                              
┌──────────────────┐         ┌──────────────────┐                    
│ Antóctica Apps   │         │  Analytics &     │                    
│  (External)      │         │  Reporting       │                    
└──────────────────┘         └──────────────────┘

Clean Architecture Layers

┌─────────────────────────────────────────────────────────────┐
│                     PRESENTATION LAYER                       │
│  • REST Controllers                                          │
│  • GraphQL Resolvers                                         │
│  • WebSocket Handlers (real-time balance)                   │
└────────────────────────┬────────────────────────────────────┘

┌────────────────────────▼────────────────────────────────────┐
│                    APPLICATION LAYER                         │
│  • Use Cases (CreateWalletUseCase, TransferTokensUseCase)   │
│  • DTOs (CreateWalletDto, TransactionDto)                   │
│  • Mappers (WalletMapper, TransactionMapper)                │
└────────────────────────┬────────────────────────────────────┘

┌────────────────────────▼────────────────────────────────────┐
│                      DOMAIN LAYER                            │
│  • Entities (Wallet, Transaction, TokenDefinition)          │
│  • Value Objects (TokenAmount, WalletAddress)               │
│  • Domain Services (TransactionService, RuleEngine)         │
│  • Domain Events (WalletCreated, TransactionCompleted)      │
│  • Repository Interfaces (WalletRepository)                 │
└────────────────────────┬────────────────────────────────────┘

┌────────────────────────▼────────────────────────────────────┐
│                  INFRASTRUCTURE LAYER                        │
│  • Repository Implementations (PrismaWalletRepository)      │
│  • Event Store (Event Sourcing)                             │
│  • External Services (PaymentGateway, NotificationService)  │
│  • Database (PostgreSQL + Redis)                            │
└─────────────────────────────────────────────────────────────┘

Modelo de Domínio

Domain Model Overview

┌────────────────────────────────────────────────────────────────────┐
│                         WALLET AGGREGATE                            │
├────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  Wallet (Aggregate Root)                                            │
│  ├── id: WalletId                                                   │
│  ├── owner: OwnerId                                                 │
│  ├── type: WalletType                                               │
│  ├── balances: TokenBalance[]                                       │
│  ├── status: WalletStatus                                           │
│  ├── createdAt: DateTime                                            │
│  └── metadata: WalletMetadata                                       │
│                                                                      │
│  TokenBalance (Entity)                                              │
│  ├── tokenDefinition: TokenDefinitionId                             │
│  ├── available: TokenAmount                                         │
│  ├── locked: TokenAmount                                            │
│  └── lastUpdated: DateTime                                          │
│                                                                      │
└────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────┐
│                      TRANSACTION AGGREGATE                          │
├────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  Transaction (Aggregate Root)                                       │
│  ├── id: TransactionId                                              │
│  ├── type: TransactionType                                          │
│  ├── from: WalletId (optional)                                      │
│  ├── to: WalletId                                                   │
│  ├── amount: TokenAmount                                            │
│  ├── token: TokenDefinitionId                                       │
│  ├── status: TransactionStatus                                      │
│  ├── reason: TransactionReason                                      │
│  ├── metadata: TransactionMetadata                                  │
│  ├── createdAt: DateTime                                            │
│  └── completedAt: DateTime                                          │
│                                                                      │
│  TransactionType:                                                   │
│  • CreditTransaction                                                │
│  • DebitTransaction                                                 │
│  • TransferTransaction                                              │
│  • RewardTransaction                                                │
│  • BetTransaction                                                   │
│  • CashbackTransaction                                              │
│  • PrizeTransaction                                                 │
│                                                                      │
└────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────┐
│                   TOKEN DEFINITION AGGREGATE                        │
├────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  TokenDefinition (Aggregate Root)                                   │
│  ├── id: TokenDefinitionId                                          │
│  ├── name: TokenName ("ArenaCoin")                                  │
│  ├── symbol: TokenSymbol ("ARC")                                    │
│  ├── description: string                                            │
│  ├── rules: TokenRules                                              │
│  ├── isActive: boolean                                              │
│  └── createdAt: DateTime                                            │
│                                                                      │
│  TokenRules (Value Object)                                          │
│  ├── exchangeRules: ExchangeRule[]                                  │
│  ├── conversionRules: ConversionRule[]                              │
│  ├── expirationRule: ExpirationRule                                 │
│  ├── rewardRules: RewardRule[]                                      │
│  ├── betRules: BetRule[]                                            │
│  └── prizeRules: PrizeRule[]                                        │
│                                                                      │
└────────────────────────────────────────────────────────────────────┘

Entidades

1. Wallet (Aggregate Root)

typescript
/**
 * Wallet - Carteira digital para armazenamento de tokens
 * 
 * Aggregate Root: Fronteira de consistência para operações de saldo
 * Invariantes:
 * - Saldo disponível nunca pode ser negativo
 * - Saldo bloqueado + disponível = saldo total
 * - Apenas uma carteira por (owner, type)
 */
class Wallet {
  private constructor(
    public readonly id: WalletId,
    private readonly _owner: OwnerId,
    private readonly _type: WalletType,
    private _balances: Map<TokenDefinitionId, TokenBalance>,
    private _status: WalletStatus,
    private readonly _createdAt: DateTime,
    private _metadata: WalletMetadata
  ) {}

  static create(props: CreateWalletProps): Wallet {
    // Validação de domínio
    if (!props.owner) {
      throw new DomainError('Wallet owner is required');
    }

    const wallet = new Wallet(
      WalletId.generate(),
      props.owner,
      props.type,
      new Map(),
      WalletStatus.ACTIVE,
      DateTime.now(),
      props.metadata || WalletMetadata.empty()
    );

    wallet.addDomainEvent(new WalletCreatedEvent(wallet.id, props.owner));

    return wallet;
  }

  // Comportamentos de domínio

  credit(amount: TokenAmount, token: TokenDefinitionId): void {
    this.ensureActive();
    
    const balance = this.getOrCreateBalance(token);
    balance.add(amount);

    this.addDomainEvent(
      new TokensCreditedEvent(this.id, token, amount)
    );
  }

  debit(amount: TokenAmount, token: TokenDefinitionId): void {
    this.ensureActive();
    
    const balance = this.getBalance(token);
    
    if (!balance.hasAvailable(amount)) {
      throw new InsufficientBalanceError(
        `Insufficient balance. Available: ${balance.available}, Required: ${amount}`
      );
    }

    balance.subtract(amount);

    this.addDomainEvent(
      new TokensDebitedEvent(this.id, token, amount)
    );
  }

  lock(amount: TokenAmount, token: TokenDefinitionId): void {
    const balance = this.getBalance(token);
    
    if (!balance.hasAvailable(amount)) {
      throw new InsufficientBalanceError();
    }

    balance.lock(amount);

    this.addDomainEvent(
      new TokensLockedEvent(this.id, token, amount)
    );
  }

  unlock(amount: TokenAmount, token: TokenDefinitionId): void {
    const balance = this.getBalance(token);
    balance.unlock(amount);

    this.addDomainEvent(
      new TokensUnlockedEvent(this.id, token, amount)
    );
  }

  suspend(reason: string): void {
    if (this._status === WalletStatus.SUSPENDED) {
      throw new DomainError('Wallet already suspended');
    }

    this._status = WalletStatus.SUSPENDED;

    this.addDomainEvent(
      new WalletSuspendedEvent(this.id, reason)
    );
  }

  activate(): void {
    if (this._status === WalletStatus.ACTIVE) {
      throw new DomainError('Wallet already active');
    }

    this._status = WalletStatus.ACTIVE;

    this.addDomainEvent(
      new WalletActivatedEvent(this.id)
    );
  }

  // Getters

  get owner(): OwnerId {
    return this._owner;
  }

  get type(): WalletType {
    return this._type;
  }

  get status(): WalletStatus {
    return this._status;
  }

  getAvailableBalance(token: TokenDefinitionId): TokenAmount {
    const balance = this._balances.get(token.value);
    return balance ? balance.available : TokenAmount.zero();
  }

  getTotalBalance(token: TokenDefinitionId): TokenAmount {
    const balance = this._balances.get(token.value);
    return balance ? balance.total : TokenAmount.zero();
  }

  // Helpers privados

  private ensureActive(): void {
    if (this._status !== WalletStatus.ACTIVE) {
      throw new InactiveWalletError('Wallet is not active');
    }
  }

  private getBalance(token: TokenDefinitionId): TokenBalance {
    const balance = this._balances.get(token.value);
    if (!balance) {
      throw new TokenNotFoundError(`Token ${token.value} not found in wallet`);
    }
    return balance;
  }

  private getOrCreateBalance(token: TokenDefinitionId): TokenBalance {
    let balance = this._balances.get(token.value);
    if (!balance) {
      balance = TokenBalance.create(token);
      this._balances.set(token.value, balance);
    }
    return balance;
  }
}

2. Transaction (Aggregate Root)

typescript
/**
 * Transaction - Movimentação de tokens entre wallets
 * 
 * Aggregate Root: Fronteira de consistência para operações transacionais
 * Invariantes:
 * - Transação completada não pode ser modificada
 * - Valor deve ser positivo
 * - From e To não podem ser iguais (exceto em edge cases)
 */
abstract class Transaction {
  protected constructor(
    public readonly id: TransactionId,
    public readonly type: TransactionType,
    protected readonly _from: WalletId | null,
    protected readonly _to: WalletId,
    protected readonly _amount: TokenAmount,
    protected readonly _token: TokenDefinitionId,
    protected _status: TransactionStatus,
    protected readonly _reason: TransactionReason,
    protected readonly _metadata: TransactionMetadata,
    protected readonly _createdAt: DateTime,
    protected _completedAt: DateTime | null
  ) {}

  // Template Method Pattern
  async execute(
    walletRepository: WalletRepository,
    ruleEngine: RuleEngine
  ): Promise<void> {
    this.ensurePending();

    try {
      // 1. Validar regras de negócio
      await this.validateBusinessRules(ruleEngine);

      // 2. Carregar wallets
      const wallets = await this.loadWallets(walletRepository);

      // 3. Aplicar transação (comportamento específico)
      await this.applyTransaction(wallets);

      // 4. Persistir wallets
      await this.persistWallets(walletRepository, wallets);

      // 5. Completar transação
      this.complete();

      // 6. Emitir evento
      this.addDomainEvent(this.createCompletedEvent());

    } catch (error) {
      this.fail(error.message);
      throw error;
    }
  }

  protected abstract validateBusinessRules(ruleEngine: RuleEngine): Promise<void>;
  protected abstract loadWallets(repo: WalletRepository): Promise<Wallet[]>;
  protected abstract applyTransaction(wallets: Wallet[]): Promise<void>;
  protected abstract createCompletedEvent(): DomainEvent;

  private complete(): void {
    this._status = TransactionStatus.COMPLETED;
    this._completedAt = DateTime.now();
  }

  private fail(reason: string): void {
    this._status = TransactionStatus.FAILED;
    this._metadata.failureReason = reason;
  }

  private ensurePending(): void {
    if (this._status !== TransactionStatus.PENDING) {
      throw new DomainError('Transaction already processed');
    }
  }

  // Getters
  get from(): WalletId | null { return this._from; }
  get to(): WalletId { return this._to; }
  get amount(): TokenAmount { return this._amount; }
  get token(): TokenDefinitionId { return this._token; }
  get status(): TransactionStatus { return this._status; }
}

/**
 * CreditTransaction - Crédito de tokens na carteira
 * Origem: Sistema (emissão) ou Arena (recompensa)
 */
class CreditTransaction extends Transaction {
  static create(props: CreateCreditTransactionProps): CreditTransaction {
    return new CreditTransaction(
      TransactionId.generate(),
      TransactionType.CREDIT,
      null, // Sistema é a origem
      props.to,
      props.amount,
      props.token,
      TransactionStatus.PENDING,
      props.reason,
      props.metadata,
      DateTime.now(),
      null
    );
  }

  protected async validateBusinessRules(ruleEngine: RuleEngine): Promise<void> {
    // Validar se o token permite emissão
    const canEmit = await ruleEngine.canEmitToken(this._token, this._amount);
    if (!canEmit) {
      throw new BusinessRuleError('Token emission not allowed');
    }
  }

  protected async loadWallets(repo: WalletRepository): Promise<Wallet[]> {
    const toWallet = await repo.findById(this._to);
    if (!toWallet) {
      throw new WalletNotFoundError(`Wallet ${this._to.value} not found`);
    }
    return [toWallet];
  }

  protected async applyTransaction(wallets: Wallet[]): Promise<void> {
    const [toWallet] = wallets;
    toWallet.credit(this._amount, this._token);
  }

  protected createCompletedEvent(): DomainEvent {
    return new CreditTransactionCompletedEvent(
      this.id,
      this._to,
      this._amount,
      this._token
    );
  }
}

/**
 * TransferTransaction - Transferência entre wallets
 */
class TransferTransaction extends Transaction {
  static create(props: CreateTransferTransactionProps): TransferTransaction {
    if (!props.from) {
      throw new DomainError('Transfer requires source wallet');
    }

    if (props.from.equals(props.to)) {
      throw new DomainError('Cannot transfer to same wallet');
    }

    return new TransferTransaction(
      TransactionId.generate(),
      TransactionType.TRANSFER,
      props.from,
      props.to,
      props.amount,
      props.token,
      TransactionStatus.PENDING,
      props.reason,
      props.metadata,
      DateTime.now(),
      null
    );
  }

  protected async validateBusinessRules(ruleEngine: RuleEngine): Promise<void> {
    // Validar se o token permite transferência
    const canTransfer = await ruleEngine.canTransferToken(
      this._token,
      this._from!,
      this._to,
      this._amount
    );
    
    if (!canTransfer) {
      throw new BusinessRuleError('Transfer not allowed by token rules');
    }
  }

  protected async loadWallets(repo: WalletRepository): Promise<Wallet[]> {
    const [fromWallet, toWallet] = await Promise.all([
      repo.findById(this._from!),
      repo.findById(this._to)
    ]);

    if (!fromWallet) {
      throw new WalletNotFoundError(`Source wallet ${this._from!.value} not found`);
    }

    if (!toWallet) {
      throw new WalletNotFoundError(`Destination wallet ${this._to.value} not found`);
    }

    return [fromWallet, toWallet];
  }

  protected async applyTransaction(wallets: Wallet[]): Promise<void> {
    const [fromWallet, toWallet] = wallets;

    // Operação atômica
    fromWallet.debit(this._amount, this._token);
    toWallet.credit(this._amount, this._token);
  }

  protected createCompletedEvent(): DomainEvent {
    return new TransferTransactionCompletedEvent(
      this.id,
      this._from!,
      this._to,
      this._amount,
      this._token
    );
  }
}

/**
 * BetTransaction - Aposta em partida/campeonato
 */
class BetTransaction extends Transaction {
  constructor(
    id: TransactionId,
    from: WalletId,
    to: WalletId, // EventWallet
    amount: TokenAmount,
    token: TokenDefinitionId,
    status: TransactionStatus,
    reason: TransactionReason,
    metadata: TransactionMetadata,
    createdAt: DateTime,
    completedAt: DateTime | null,
    private readonly _matchId: MatchId,
    private readonly _prediction: BetPrediction
  ) {
    super(id, TransactionType.BET, from, to, amount, token, status, reason, metadata, createdAt, completedAt);
  }

  static create(props: CreateBetTransactionProps): BetTransaction {
    return new BetTransaction(
      TransactionId.generate(),
      props.from,
      props.eventWallet,
      props.amount,
      props.token,
      TransactionStatus.PENDING,
      TransactionReason.BET,
      props.metadata,
      DateTime.now(),
      null,
      props.matchId,
      props.prediction
    );
  }

  protected async validateBusinessRules(ruleEngine: RuleEngine): Promise<void> {
    // Validar regras de aposta
    const canBet = await ruleEngine.canPlaceBet(
      this._matchId,
      this._from!,
      this._amount,
      this._token
    );

    if (!canBet) {
      throw new BusinessRuleError('Bet not allowed');
    }
  }

  protected async loadWallets(repo: WalletRepository): Promise<Wallet[]> {
    const [fromWallet, eventWallet] = await Promise.all([
      repo.findById(this._from!),
      repo.findById(this._to)
    ]);

    if (!fromWallet) throw new WalletNotFoundError();
    if (!eventWallet) throw new WalletNotFoundError();

    return [fromWallet, eventWallet];
  }

  protected async applyTransaction(wallets: Wallet[]): Promise<void> {
    const [fromWallet, eventWallet] = wallets;

    // Lock tokens na carteira do apostador
    fromWallet.lock(this._amount, this._token);

    // Creditar na pool do evento
    eventWallet.credit(this._amount, this._token);
  }

  protected createCompletedEvent(): DomainEvent {
    return new BetPlacedEvent(
      this.id,
      this._from!,
      this._matchId,
      this._amount,
      this._prediction
    );
  }

  get matchId(): MatchId {
    return this._matchId;
  }

  get prediction(): BetPrediction {
    return this._prediction;
  }
}

3. TokenDefinition (Aggregate Root)

typescript
/**
 * TokenDefinition - Definição e regras de um token
 * 
 * Aggregate Root: Fronteira de consistência para configuração de tokens
 */
class TokenDefinition {
  private constructor(
    public readonly id: TokenDefinitionId,
    private _name: TokenName,
    private _symbol: TokenSymbol,
    private _description: string,
    private _rules: TokenRules,
    private _isActive: boolean,
    private readonly _createdAt: DateTime
  ) {}

  static create(props: CreateTokenDefinitionProps): TokenDefinition {
    const tokenDef = new TokenDefinition(
      TokenDefinitionId.generate(),
      props.name,
      props.symbol,
      props.description,
      props.rules,
      true,
      DateTime.now()
    );

    tokenDef.addDomainEvent(
      new TokenDefinitionCreatedEvent(tokenDef.id, props.name, props.symbol)
    );

    return tokenDef;
  }

  // Factory method para ArenaCoin padrão
  static createArenaCoin(): TokenDefinition {
    return TokenDefinition.create({
      name: TokenName.create('ArenaCoin'),
      symbol: TokenSymbol.create('ARC'),
      description: 'Moeda oficial da Sport Tech Arena',
      rules: TokenRules.createDefault()
    });
  }

  updateRules(rules: TokenRules): void {
    this._rules = rules;

    this.addDomainEvent(
      new TokenRulesUpdatedEvent(this.id, rules)
    );
  }

  deactivate(): void {
    this._isActive = false;

    this.addDomainEvent(
      new TokenDefinitionDeactivatedEvent(this.id)
    );
  }

  // Validações de regras

  canTransfer(from: WalletId, to: WalletId, amount: TokenAmount): boolean {
    if (!this._isActive) return false;

    // Verificar regras de transferência
    return this._rules.exchangeRules.every(rule => 
      rule.allowsTransfer(from, to, amount)
    );
  }

  canConvertToReal(amount: TokenAmount): boolean {
    return this._rules.conversionRules.some(rule => 
      rule.allowsConversion(amount)
    );
  }

  isExpired(balance: TokenBalance): boolean {
    if (!this._rules.expirationRule) return false;
    return this._rules.expirationRule.isExpired(balance.lastUpdated);
  }

  calculateReward(event: RewardEvent): TokenAmount {
    const applicableRules = this._rules.rewardRules.filter(rule =>
      rule.appliesTo(event)
    );

    const amounts = applicableRules.map(rule => rule.calculate(event));
    return TokenAmount.sum(amounts);
  }

  // Getters
  get name(): TokenName { return this._name; }
  get symbol(): TokenSymbol { return this._symbol; }
  get description(): string { return this._description; }
  get rules(): TokenRules { return this._rules; }
  get isActive(): boolean { return this._isActive; }
}

Value Objects

TokenAmount

typescript
/**
 * TokenAmount - Quantidade de tokens
 * Value Object: Imutável, sem identidade
 */
class TokenAmount {
  private constructor(private readonly value: number) {
    if (value < 0) {
      throw new DomainError('Token amount cannot be negative');
    }

    if (!Number.isFinite(value)) {
      throw new DomainError('Token amount must be finite');
    }
  }

  static create(value: number): TokenAmount {
    return new TokenAmount(value);
  }

  static zero(): TokenAmount {
    return new TokenAmount(0);
  }

  static sum(amounts: TokenAmount[]): TokenAmount {
    const total = amounts.reduce((acc, amount) => acc + amount.value, 0);
    return new TokenAmount(total);
  }

  add(other: TokenAmount): TokenAmount {
    return new TokenAmount(this.value + other.value);
  }

  subtract(other: TokenAmount): TokenAmount {
    return new TokenAmount(this.value - other.value);
  }

  multiply(factor: number): TokenAmount {
    return new TokenAmount(this.value * factor);
  }

  isGreaterThan(other: TokenAmount): boolean {
    return this.value > other.value;
  }

  isGreaterThanOrEqual(other: TokenAmount): boolean {
    return this.value >= other.value;
  }

  equals(other: TokenAmount): boolean {
    return this.value === other.value;
  }

  toNumber(): number {
    return this.value;
  }

  toString(): string {
    return this.value.toFixed(2);
  }
}

WalletType

typescript
/**
 * WalletType - Tipo de carteira
 */
enum WalletType {
  USER = 'USER',           // Carteira de jogador/aluno
  ARENA = 'ARENA',         // Carteira da arena
  EVENT = 'EVENT',         // Carteira temporária de evento
  APPLICATION = 'APPLICATION' // Carteira de integração externa
}

class WalletTypeVO {
  private constructor(private readonly value: WalletType) {}

  static create(value: string): WalletTypeVO {
    if (!Object.values(WalletType).includes(value as WalletType)) {
      throw new DomainError(`Invalid wallet type: ${value}`);
    }
    return new WalletTypeVO(value as WalletType);
  }

  static user(): WalletTypeVO {
    return new WalletTypeVO(WalletType.USER);
  }

  static arena(): WalletTypeVO {
    return new WalletTypeVO(WalletType.ARENA);
  }

  static event(): WalletTypeVO {
    return new WalletTypeVO(WalletType.EVENT);
  }

  static application(): WalletTypeVO {
    return new WalletTypeVO(WalletType.APPLICATION);
  }

  isUser(): boolean {
    return this.value === WalletType.USER;
  }

  isArena(): boolean {
    return this.value === WalletType.ARENA;
  }

  equals(other: WalletTypeVO): boolean {
    return this.value === other.value;
  }

  toString(): string {
    return this.value;
  }
}

Regras de Negócio

ExchangeRule - Conversão entre Valores

typescript
/**
 * ExchangeRule - Regra de troca/conversão entre tipos de valor
 * 
 * Exemplos:
 * - 100 ARC = 1 hora de quadra
 * - 50 ARC = 1 aula particular
 * - 200 ARC = 1 uniforme
 */
class ExchangeRule {
  constructor(
    public readonly id: string,
    private readonly _fromToken: TokenDefinitionId,
    private readonly _toType: ExchangeType,
    private readonly _rate: ExchangeRate,
    private readonly _minAmount: TokenAmount,
    private readonly _maxAmount: TokenAmount | null,
    private readonly _isActive: boolean
  ) {}

  static create(props: CreateExchangeRuleProps): ExchangeRule {
    return new ExchangeRule(
      crypto.randomUUID(),
      props.fromToken,
      props.toType,
      props.rate,
      props.minAmount,
      props.maxAmount || null,
      true
    );
  }

  // Exemplo: Troca de ARC por hora de quadra
  static arenaCourtHour(): ExchangeRule {
    return ExchangeRule.create({
      fromToken: TokenDefinitionId.arenaCoin(),
      toType: ExchangeType.COURT_HOUR,
      rate: ExchangeRate.create(100, 1), // 100 ARC = 1 hora
      minAmount: TokenAmount.create(100)
    });
  }

  canExchange(amount: TokenAmount): boolean {
    if (!this._isActive) return false;
    if (amount.isLessThan(this._minAmount)) return false;
    if (this._maxAmount && amount.isGreaterThan(this._maxAmount)) return false;
    return true;
  }

  calculateExchange(amount: TokenAmount): number {
    return this._rate.calculate(amount);
  }

  allowsTransfer(from: WalletId, to: WalletId, amount: TokenAmount): boolean {
    return this.canExchange(amount);
  }
}

enum ExchangeType {
  COURT_HOUR = 'COURT_HOUR',
  PRIVATE_CLASS = 'PRIVATE_CLASS',
  UNIFORM = 'UNIFORM',
  EQUIPMENT = 'EQUIPMENT',
  FOOD = 'FOOD',
  BEVERAGE = 'BEVERAGE'
}

class ExchangeRate {
  constructor(
    private readonly tokenAmount: number,
    private readonly valueAmount: number
  ) {}

  static create(tokenAmount: number, valueAmount: number): ExchangeRate {
    if (tokenAmount <= 0 || valueAmount <= 0) {
      throw new DomainError('Exchange rate must be positive');
    }
    return new ExchangeRate(tokenAmount, valueAmount);
  }

  calculate(amount: TokenAmount): number {
    return (amount.toNumber() / this.tokenAmount) * this.valueAmount;
  }
}

RewardRule - Regras de Recompensa

typescript
/**
 * RewardRule - Regra de recompensa por ação/conquista
 * 
 * Exemplos:
 * - Participar de campeonato: 50 ARC
 * - Vencer partida: 100 ARC
 * - Fazer check-in: 10 ARC
 * - Trazer amigo: 200 ARC
 */
class RewardRule {
  constructor(
    public readonly id: string,
    private readonly _eventType: RewardEventType,
    private readonly _token: TokenDefinitionId,
    private readonly _amount: TokenAmount,
    private readonly _conditions: RewardCondition[],
    private readonly _maxPerUser: number | null,
    private readonly _isActive: boolean
  ) {}

  static participationReward(): RewardRule {
    return new RewardRule(
      crypto.randomUUID(),
      RewardEventType.TOURNAMENT_PARTICIPATION,
      TokenDefinitionId.arenaCoin(),
      TokenAmount.create(50),
      [],
      null, // Sem limite
      true
    );
  }

  static victoryReward(): RewardRule {
    return new RewardRule(
      crypto.randomUUID(),
      RewardEventType.MATCH_VICTORY,
      TokenDefinitionId.arenaCoin(),
      TokenAmount.create(100),
      [
        new RewardCondition('official_match', true)
      ],
      null,
      true
    );
  }

  static checkInReward(): RewardRule {
    return new RewardRule(
      crypto.randomUUID(),
      RewardEventType.CHECK_IN,
      TokenDefinitionId.arenaCoin(),
      TokenAmount.create(10),
      [],
      30, // Máximo 30 por mês
      true
    );
  }

  appliesTo(event: RewardEvent): boolean {
    if (!this._isActive) return false;
    if (event.type !== this._eventType) return false;
    return this._conditions.every(cond => cond.isMet(event));
  }

  calculate(event: RewardEvent): TokenAmount {
    if (!this.appliesTo(event)) {
      return TokenAmount.zero();
    }

    // Aplicar multiplicadores baseados no evento
    let multiplier = 1;

    if (event.metadata.isFirstTime) {
      multiplier *= 2; // Dobra na primeira vez
    }

    if (event.metadata.streak && event.metadata.streak > 5) {
      multiplier *= 1.5; // Bônus por sequência
    }

    return this._amount.multiply(multiplier);
  }
}

enum RewardEventType {
  CHECK_IN = 'CHECK_IN',
  MATCH_VICTORY = 'MATCH_VICTORY',
  TOURNAMENT_PARTICIPATION = 'TOURNAMENT_PARTICIPATION',
  TOURNAMENT_VICTORY = 'TOURNAMENT_VICTORY',
  REFERRAL = 'REFERRAL',
  ACHIEVEMENT_UNLOCKED = 'ACHIEVEMENT_UNLOCKED'
}

interface RewardEvent {
  type: RewardEventType;
  userId: string;
  metadata: Record<string, any>;
  occurredAt: DateTime;
}

class RewardCondition {
  constructor(
    private readonly field: string,
    private readonly expectedValue: any
  ) {}

  isMet(event: RewardEvent): boolean {
    return event.metadata[this.field] === this.expectedValue;
  }
}

BetRule - Regras de Apostas

typescript
/**
 * BetRule - Regra de aposta
 * 
 * Define:
 * - Limite mínimo/máximo de aposta
 * - Odds/multiplicadores
 * - Prazo para apostar
 * - Distribuição de prêmios
 */
class BetRule {
  constructor(
    public readonly id: string,
    private readonly _minBet: TokenAmount,
    private readonly _maxBet: TokenAmount,
    private readonly _deadlineBeforeMatch: Duration,
    private readonly _prizeDistribution: PrizeDistribution,
    private readonly _isActive: boolean
  ) {}

  static createDefault(): BetRule {
    return new BetRule(
      crypto.randomUUID(),
      TokenAmount.create(10),  // Mínimo 10 ARC
      TokenAmount.create(500), // Máximo 500 ARC
      Duration.fromMinutes(30), // Encerra 30min antes
      PrizeDistribution.winnerTakesAll(),
      true
    );
  }

  canPlaceBet(
    matchId: MatchId,
    amount: TokenAmount,
    matchStartTime: DateTime
  ): boolean {
    if (!this._isActive) return false;

    // Verificar valores
    if (amount.isLessThan(this._minBet)) return false;
    if (amount.isGreaterThan(this._maxBet)) return false;

    // Verificar prazo
    const now = DateTime.now();
    const deadline = matchStartTime.subtract(this._deadlineBeforeMatch);
    
    if (now.isAfter(deadline)) return false;

    return true;
  }

  calculatePrize(
    totalPool: TokenAmount,
    winningBets: BetTransaction[]
  ): Map<WalletId, TokenAmount> {
    return this._prizeDistribution.distribute(totalPool, winningBets);
  }
}

class PrizeDistribution {
  constructor(
    private readonly strategy: PrizeDistributionStrategy
  ) {}

  static winnerTakesAll(): PrizeDistribution {
    return new PrizeDistribution(
      PrizeDistributionStrategy.WINNER_TAKES_ALL
    );
  }

  static proportional(): PrizeDistribution {
    return new PrizeDistribution(
      PrizeDistributionStrategy.PROPORTIONAL
    );
  }

  distribute(
    totalPool: TokenAmount,
    winningBets: BetTransaction[]
  ): Map<WalletId, TokenAmount> {
    const prizes = new Map<WalletId, TokenAmount>();

    if (winningBets.length === 0) {
      return prizes;
    }

    switch (this.strategy) {
      case PrizeDistributionStrategy.WINNER_TAKES_ALL:
        // Divide igualmente entre vencedores
        const prizePerWinner = totalPool.divide(winningBets.length);
        winningBets.forEach(bet => {
          prizes.set(bet.from!, prizePerWinner);
        });
        break;

      case PrizeDistributionStrategy.PROPORTIONAL:
        // Proporcional ao valor apostado
        const totalBetAmount = TokenAmount.sum(
          winningBets.map(bet => bet.amount)
        );

        winningBets.forEach(bet => {
          const proportion = bet.amount.toNumber() / totalBetAmount.toNumber();
          const prize = totalPool.multiply(proportion);
          prizes.set(bet.from!, prize);
        });
        break;
    }

    return prizes;
  }
}

enum PrizeDistributionStrategy {
  WINNER_TAKES_ALL = 'WINNER_TAKES_ALL',
  PROPORTIONAL = 'PROPORTIONAL'
}

ExpirationRule - Expiração de Tokens

typescript
/**
 * ExpirationRule - Regra de expiração de tokens
 * 
 * Exemplos:
 * - Tokens de cashback expiram em 90 dias
 * - Tokens de campeonato não expiram
 */
class ExpirationRule {
  constructor(
    private readonly _enabled: boolean,
    private readonly _expirationPeriod: Duration | null,
    private readonly _warningPeriod: Duration | null
  ) {}

  static noExpiration(): ExpirationRule {
    return new ExpirationRule(false, null, null);
  }

  static expireAfter(period: Duration, warningPeriod?: Duration): ExpirationRule {
    return new ExpirationRule(
      true,
      period,
      warningPeriod || Duration.fromDays(7)
    );
  }

  isExpired(lastUpdated: DateTime): boolean {
    if (!this._enabled || !this._expirationPeriod) {
      return false;
    }

    const expirationDate = lastUpdated.add(this._expirationPeriod);
    return DateTime.now().isAfter(expirationDate);
  }

  shouldWarn(lastUpdated: DateTime): boolean {
    if (!this._enabled || !this._expirationPeriod || !this._warningPeriod) {
      return false;
    }

    const warningDate = lastUpdated
      .add(this._expirationPeriod)
      .subtract(this._warningPeriod);

    const now = DateTime.now();
    return now.isAfter(warningDate) && !this.isExpired(lastUpdated);
  }
}

Casos de Uso

1. CreateWalletUseCase

typescript
/**
 * CreateWalletUseCase - Criar carteira para usuário/arena/evento
 */
class CreateWalletUseCase {
  constructor(
    private readonly walletRepository: WalletRepository,
    private readonly eventDispatcher: EventDispatcher
  ) {}

  async execute(request: CreateWalletRequest): Promise<CreateWalletResponse> {
    // 1. Validar se já existe carteira
    const existing = await this.walletRepository.findByOwnerAndType(
      request.owner,
      request.type
    );

    if (existing) {
      throw new WalletAlreadyExistsError(
        `Wallet already exists for owner ${request.owner.value} and type ${request.type}`
      );
    }

    // 2. Criar wallet
    const wallet = Wallet.create({
      owner: request.owner,
      type: request.type,
      metadata: request.metadata
    });

    // 3. Persistir
    await this.walletRepository.save(wallet);

    // 4. Disparar eventos
    await this.eventDispatcher.dispatchAll(wallet.getDomainEvents());

    // 5. Retornar
    return {
      walletId: wallet.id,
      owner: wallet.owner,
      type: wallet.type,
      createdAt: wallet.createdAt
    };
  }
}

interface CreateWalletRequest {
  owner: OwnerId;
  type: WalletTypeVO;
  metadata?: WalletMetadata;
}

interface CreateWalletResponse {
  walletId: WalletId;
  owner: OwnerId;
  type: WalletTypeVO;
  createdAt: DateTime;
}

2. TransferTokensUseCase

typescript
/**
 * TransferTokensUseCase - Transferir tokens entre wallets
 */
class TransferTokensUseCase {
  constructor(
    private readonly walletRepository: WalletRepository,
    private readonly transactionRepository: TransactionRepository,
    private readonly ruleEngine: RuleEngine,
    private readonly eventDispatcher: EventDispatcher
  ) {}

  async execute(request: TransferTokensRequest): Promise<TransferTokensResponse> {
    // 1. Criar transação
    const transaction = TransferTransaction.create({
      from: request.fromWallet,
      to: request.toWallet,
      amount: request.amount,
      token: request.token,
      reason: request.reason,
      metadata: request.metadata
    });

    // 2. Executar transação (Template Method)
    await transaction.execute(this.walletRepository, this.ruleEngine);

    // 3. Persistir transação
    await this.transactionRepository.save(transaction);

    // 4. Disparar eventos
    await this.eventDispatcher.dispatchAll(transaction.getDomainEvents());

    // 5. Retornar
    return {
      transactionId: transaction.id,
      status: transaction.status,
      completedAt: transaction.completedAt!
    };
  }
}

interface TransferTokensRequest {
  fromWallet: WalletId;
  toWallet: WalletId;
  amount: TokenAmount;
  token: TokenDefinitionId;
  reason: TransactionReason;
  metadata?: TransactionMetadata;
}

interface TransferTokensResponse {
  transactionId: TransactionId;
  status: TransactionStatus;
  completedAt: DateTime;
}

3. PlaceBetUseCase

typescript
/**
 * PlaceBetUseCase - Realizar aposta em partida
 */
class PlaceBetUseCase {
  constructor(
    private readonly walletRepository: WalletRepository,
    private readonly transactionRepository: TransactionRepository,
    private readonly matchRepository: MatchRepository,
    private readonly ruleEngine: RuleEngine,
    private readonly eventDispatcher: EventDispatcher
  ) {}

  async execute(request: PlaceBetRequest): Promise<PlaceBetResponse> {
    // 1. Carregar partida e validar
    const match = await this.matchRepository.findById(request.matchId);
    if (!match) {
      throw new MatchNotFoundError();
    }

    if (match.hasStarted()) {
      throw new BusinessRuleError('Cannot bet on started match');
    }

    // 2. Obter/criar EventWallet para a partida
    let eventWallet = await this.walletRepository.findByOwnerAndType(
      request.matchId,
      WalletTypeVO.event()
    );

    if (!eventWallet) {
      eventWallet = Wallet.create({
        owner: request.matchId,
        type: WalletTypeVO.event(),
        metadata: WalletMetadata.forMatch(request.matchId)
      });
      await this.walletRepository.save(eventWallet);
    }

    // 3. Criar transação de aposta
    const betTransaction = BetTransaction.create({
      from: request.userWallet,
      eventWallet: eventWallet.id,
      amount: request.amount,
      token: request.token,
      matchId: request.matchId,
      prediction: request.prediction,
      metadata: TransactionMetadata.forBet(request.matchId)
    });

    // 4. Executar
    await betTransaction.execute(this.walletRepository, this.ruleEngine);

    // 5. Persistir
    await this.transactionRepository.save(betTransaction);

    // 6. Disparar eventos
    await this.eventDispatcher.dispatchAll(betTransaction.getDomainEvents());

    // 7. Retornar
    return {
      betId: betTransaction.id,
      matchId: request.matchId,
      amount: request.amount,
      prediction: request.prediction,
      lockedUntil: match.startTime
    };
  }
}

interface PlaceBetRequest {
  userWallet: WalletId;
  matchId: MatchId;
  amount: TokenAmount;
  token: TokenDefinitionId;
  prediction: BetPrediction;
}

interface PlaceBetResponse {
  betId: TransactionId;
  matchId: MatchId;
  amount: TokenAmount;
  prediction: BetPrediction;
  lockedUntil: DateTime;
}

class BetPrediction {
  constructor(
    public readonly type: BetPredictionType,
    public readonly teamId?: string
  ) {}

  static homeWin(teamId: string): BetPrediction {
    return new BetPrediction(BetPredictionType.HOME_WIN, teamId);
  }

  static awayWin(teamId: string): BetPrediction {
    return new BetPrediction(BetPredictionType.AWAY_WIN, teamId);
  }

  static draw(): BetPrediction {
    return new BetPrediction(BetPredictionType.DRAW);
  }
}

enum BetPredictionType {
  HOME_WIN = 'HOME_WIN',
  AWAY_WIN = 'AWAY_WIN',
  DRAW = 'DRAW'
}

4. RewardPlayerUseCase

typescript
/**
 * RewardPlayerUseCase - Recompensar jogador por conquista
 */
class RewardPlayerUseCase {
  constructor(
    private readonly walletRepository: WalletRepository,
    private readonly transactionRepository: TransactionRepository,
    private readonly tokenDefinitionRepository: TokenDefinitionRepository,
    private readonly eventDispatcher: EventDispatcher
  ) {}

  async execute(request: RewardPlayerRequest): Promise<RewardPlayerResponse> {
    // 1. Carregar token definition e calcular recompensa
    const tokenDef = await this.tokenDefinitionRepository.findById(
      request.token
    );

    if (!tokenDef) {
      throw new TokenNotFoundError();
    }

    const rewardAmount = tokenDef.calculateReward(request.rewardEvent);

    if (rewardAmount.equals(TokenAmount.zero())) {
      throw new BusinessRuleError('No reward applicable for this event');
    }

    // 2. Carregar ArenaWallet (fonte dos tokens)
    const arenaWallet = await this.walletRepository.findByType(
      WalletTypeVO.arena()
    );

    if (!arenaWallet) {
      throw new WalletNotFoundError('Arena wallet not found');
    }

    // 3. Criar transação de recompensa
    const rewardTransaction = RewardTransaction.create({
      from: arenaWallet.id,
      to: request.playerWallet,
      amount: rewardAmount,
      token: request.token,
      reason: TransactionReason.REWARD,
      metadata: TransactionMetadata.forReward(request.rewardEvent)
    });

    // 4. Executar
    await rewardTransaction.execute(
      this.walletRepository,
      new NoOpRuleEngine() // Rewards não passam por validação
    );

    // 5. Persistir
    await this.transactionRepository.save(rewardTransaction);

    // 6. Disparar eventos
    await this.eventDispatcher.dispatchAll(rewardTransaction.getDomainEvents());

    // 7. Retornar
    return {
      rewardId: rewardTransaction.id,
      amount: rewardAmount,
      reason: request.rewardEvent.type
    };
  }
}

interface RewardPlayerRequest {
  playerWallet: WalletId;
  token: TokenDefinitionId;
  rewardEvent: RewardEvent;
}

interface RewardPlayerResponse {
  rewardId: TransactionId;
  amount: TokenAmount;
  reason: RewardEventType;
}

5. SettleBetsUseCase

typescript
/**
 * SettleBetsUseCase - Liquidar apostas após fim da partida
 */
class SettleBetsUseCase {
  constructor(
    private readonly walletRepository: WalletRepository,
    private readonly transactionRepository: TransactionRepository,
    private readonly matchRepository: MatchRepository,
    private readonly betRepository: BetRepository,
    private readonly tokenDefinitionRepository: TokenDefinitionRepository,
    private readonly eventDispatcher: EventDispatcher
  ) {}

  async execute(request: SettleBetsRequest): Promise<SettleBetsResponse> {
    // 1. Carregar partida e validar
    const match = await this.matchRepository.findById(request.matchId);
    if (!match) {
      throw new MatchNotFoundError();
    }

    if (!match.isFinished()) {
      throw new BusinessRuleError('Match not finished yet');
    }

    // 2. Carregar EventWallet
    const eventWallet = await this.walletRepository.findByOwnerAndType(
      request.matchId,
      WalletTypeVO.event()
    );

    if (!eventWallet) {
      throw new WalletNotFoundError('Event wallet not found');
    }

    // 3. Carregar todas as apostas
    const allBets = await this.betRepository.findByMatch(request.matchId);

    // 4. Determinar apostas vencedoras
    const winningBets = allBets.filter(bet => 
      this.isWinningBet(bet, match.result)
    );

    if (winningBets.length === 0) {
      // Sem vencedores, arena fica com o pool
      return {
        totalBets: allBets.length,
        winningBets: 0,
        totalPrize: TokenAmount.zero()
      };
    }

    // 5. Obter regras de premiação
    const tokenDef = await this.tokenDefinitionRepository.findById(
      allBets[0].token
    );

    const betRule = tokenDef!.rules.betRules[0]; // Simplificado

    // 6. Calcular distribuição de prêmios
    const totalPool = eventWallet.getAvailableBalance(allBets[0].token);
    const prizes = betRule.calculatePrize(totalPool, winningBets);

    // 7. Criar transações de prêmio para cada vencedor
    const prizeTransactions: PrizeTransaction[] = [];

    for (const [winnerId, prizeAmount] of prizes.entries()) {
      const prizeTransaction = PrizeTransaction.create({
        from: eventWallet.id,
        to: winnerId,
        amount: prizeAmount,
        token: allBets[0].token,
        reason: TransactionReason.PRIZE,
        metadata: TransactionMetadata.forPrize(request.matchId)
      });

      await prizeTransaction.execute(
        this.walletRepository,
        new NoOpRuleEngine()
      );

      prizeTransactions.push(prizeTransaction);
    }

    // 8. Persistir transações
    await this.transactionRepository.saveMany(prizeTransactions);

    // 9. Unlock tokens dos perdedores (já foram debitados)
    const losingBets = allBets.filter(bet => !winningBets.includes(bet));
    
    for (const losingBet of losingBets) {
      const wallet = await this.walletRepository.findById(losingBet.from!);
      wallet!.unlock(losingBet.amount, losingBet.token);
      await this.walletRepository.save(wallet!);
    }

    // 10. Disparar eventos
    await this.eventDispatcher.dispatch(
      new BetsSettledEvent(
        request.matchId,
        allBets.length,
        winningBets.length,
        totalPool
      )
    );

    // 11. Retornar
    return {
      totalBets: allBets.length,
      winningBets: winningBets.length,
      totalPrize: totalPool
    };
  }

  private isWinningBet(bet: BetTransaction, result: MatchResult): boolean {
    switch (bet.prediction.type) {
      case BetPredictionType.HOME_WIN:
        return result.winner === 'HOME';
      case BetPredictionType.AWAY_WIN:
        return result.winner === 'AWAY';
      case BetPredictionType.DRAW:
        return result.winner === 'DRAW';
      default:
        return false;
    }
  }
}

interface SettleBetsRequest {
  matchId: MatchId;
}

interface SettleBetsResponse {
  totalBets: number;
  winningBets: number;
  totalPrize: TokenAmount;
}

Fluxos de Transação

Fluxo 1: Jogador Ganha Recompensa por Participação

┌─────────────┐
│   Player    │
│  Finishes   │
│   Match     │
└──────┬──────┘


┌──────────────────────────────────────────────┐
│  1. MatchFinishedEvent Dispatched            │
└──────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  2. RewardEventHandler Listens               │
│     - Identify reward rules applicable       │
│     - Calculate reward amount (50 ARC)       │
└──────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  3. RewardPlayerUseCase.execute()            │
│     - Load ArenaWallet                       │
│     - Load PlayerWallet                      │
│     - Create RewardTransaction               │
└──────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  4. Transaction.execute()                    │
│     - ArenaWallet.debit(50 ARC)              │
│     - PlayerWallet.credit(50 ARC)            │
└──────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  5. Events Dispatched                        │
│     - TokensCreditedEvent                    │
│     - RewardTransactionCompletedEvent        │
└──────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  6. Notification Sent to Player              │
│     "You earned 50 ARC for playing!"         │
└──────────────────────────────────────────────┘

Fluxo 2: Jogador Aposta em Partida

┌─────────────┐
│   Player    │
│ Places Bet  │
│  100 ARC    │
└──────┬──────┘


┌──────────────────────────────────────────────┐
│  1. PlaceBetUseCase.execute()                │
│     - Validate match not started             │
│     - Validate bet amount (min/max)          │
└──────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  2. Load/Create EventWallet for match        │
└──────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  3. Create BetTransaction                    │
│     - From: PlayerWallet                     │
│     - To: EventWallet                        │
│     - Amount: 100 ARC                        │
│     - Prediction: HomeWin                    │
└──────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  4. Transaction.execute()                    │
│     - PlayerWallet.lock(100 ARC)             │
│     - EventWallet.credit(100 ARC)            │
└──────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  5. BetPlacedEvent Dispatched                │
└──────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  6. Notification Sent                        │
│     "Bet placed! 100 ARC locked"             │
└──────────────────────────────────────────────┘

       │  (Wait for match to finish)


┌──────────────────────────────────────────────┐
│  7. Match Finishes                           │
│     - MatchFinishedEvent                     │
└──────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  8. SettleBetsUseCase.execute()              │
│     - Identify winning bets                  │
│     - Calculate prize distribution           │
│     - Transfer prizes to winners             │
│     - Unlock tokens for losers               │
└──────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  9. If Won:                                  │
│     PlayerWallet.credit(200 ARC) + unlock    │
│                                              │
│     If Lost:                                 │
│     PlayerWallet.unlock(100 ARC already gone)│
└──────────────────────────────────────────────┘

Fluxo 3: Patrocinador Financia Prêmios

┌─────────────┐
│  Sponsor    │
│  Deposits   │
│ 10,000 ARC  │
└──────┬──────┘


┌──────────────────────────────────────────────┐
│  1. CreateSponsorWalletUseCase               │
│     (if not exists)                          │
└──────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  2. Admin converts real money to ARC         │
│     $1000 USD → 10,000 ARC                   │
│     (via ConversionRule)                     │
└──────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  3. CreditTransaction to SponsorWallet       │
│     - System mints 10,000 ARC                │
│     - Credits SponsorWallet                  │
└──────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  4. TransferToArenaWallet                    │
│     - From: SponsorWallet                    │
│     - To: ArenaWallet                        │
│     - Amount: 10,000 ARC                     │
│     - Reason: TOURNAMENT_PRIZE               │
└──────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  5. Tournament Prizes Now Available          │
│     ArenaWallet balance += 10,000 ARC        │
└──────────────────────────────────────────────┘

Fluxo 4: Jogador Troca ARC por Hora de Quadra

┌─────────────┐
│   Player    │
│  Requests   │
│ Court Hour  │
└──────┬──────┘


┌──────────────────────────────────────────────┐
│  1. ExchangeTokensUseCase.execute()          │
│     - exchangeType: COURT_HOUR               │
│     - amount: 100 ARC                        │
└──────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  2. Validate ExchangeRule                    │
│     - 100 ARC = 1 Court Hour                 │
│     - Min: 100 ARC (✓)                       │
│     - Player has balance (✓)                 │
└──────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  3. Create TransferTransaction               │
│     - From: PlayerWallet                     │
│     - To: ArenaWallet                        │
│     - Amount: 100 ARC                        │
│     - Reason: EXCHANGE_COURT_HOUR            │
└──────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  4. Transaction.execute()                    │
│     - PlayerWallet.debit(100 ARC)            │
│     - ArenaWallet.credit(100 ARC)            │
└──────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  5. Create Court Reservation Credit          │
│     (Integration with Booking System)        │
│     - User gets 1 hour credit                │
└──────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  6. ExchangeCompletedEvent Dispatched        │
└──────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  7. Notification Sent                        │
│     "You exchanged 100 ARC for 1 court hour" │
└──────────────────────────────────────────────┘

Segurança e Auditoria

Event Sourcing & Ledger

typescript
/**
 * Event Store - Armazena todos os eventos de domínio
 * Garante auditabilidade completa e rastreabilidade
 */
class EventStore {
  constructor(private readonly db: PrismaClient) {}

  async append(events: DomainEvent[]): Promise<void> {
    const records = events.map(event => ({
      id: crypto.randomUUID(),
      aggregateId: event.aggregateId,
      aggregateType: event.aggregateType,
      eventType: event.constructor.name,
      eventData: JSON.stringify(event),
      metadata: JSON.stringify(event.metadata),
      occurredAt: event.occurredAt.toISOString(),
      version: event.version
    }));

    await this.db.eventStoreEntry.createMany({
      data: records
    });
  }

  async getEventsForAggregate(
    aggregateId: string,
    fromVersion: number = 0
  ): Promise<DomainEvent[]> {
    const records = await this.db.eventStoreEntry.findMany({
      where: {
        aggregateId,
        version: { gte: fromVersion }
      },
      orderBy: { version: 'asc' }
    });

    return records.map(record => this.deserializeEvent(record));
  }

  async getAllEvents(
    filters?: EventFilters,
    pagination?: Pagination
  ): Promise<DomainEvent[]> {
    const records = await this.db.eventStoreEntry.findMany({
      where: this.buildWhereClause(filters),
      orderBy: { occurredAt: 'asc' },
      skip: pagination?.offset,
      take: pagination?.limit
    });

    return records.map(record => this.deserializeEvent(record));
  }

  private deserializeEvent(record: EventStoreEntry): DomainEvent {
    const EventClass = this.getEventClass(record.eventType);
    const eventData = JSON.parse(record.eventData);
    return new EventClass(eventData);
  }

  private getEventClass(eventType: string): any {
    // Event Registry Pattern
    return EventRegistry.get(eventType);
  }
}

/**
 * Transaction Ledger - View materializada para queries rápidas
 */
class TransactionLedger {
  constructor(private readonly db: PrismaClient) {}

  async record(transaction: Transaction): Promise<void> {
    await this.db.transactionLedger.create({
      data: {
        transactionId: transaction.id.value,
        type: transaction.type,
        from: transaction.from?.value,
        to: transaction.to.value,
        amount: transaction.amount.toNumber(),
        token: transaction.token.value,
        status: transaction.status,
        reason: transaction.reason,
        metadata: transaction.metadata,
        createdAt: transaction.createdAt.toDate(),
        completedAt: transaction.completedAt?.toDate()
      }
    });
  }

  async getHistory(
    walletId: WalletId,
    filters?: TransactionFilters
  ): Promise<TransactionLedgerEntry[]> {
    return this.db.transactionLedger.findMany({
      where: {
        OR: [
          { from: walletId.value },
          { to: walletId.value }
        ],
        ...this.buildFilters(filters)
      },
      orderBy: { createdAt: 'desc' }
    });
  }

  async getAuditTrail(
    startDate: DateTime,
    endDate: DateTime
  ): Promise<AuditReport> {
    const entries = await this.db.transactionLedger.findMany({
      where: {
        createdAt: {
          gte: startDate.toDate(),
          lte: endDate.toDate()
        }
      }
    });

    return this.generateAuditReport(entries);
  }

  private generateAuditReport(entries: TransactionLedgerEntry[]): AuditReport {
    return {
      totalTransactions: entries.length,
      totalVolume: entries.reduce((sum, e) => sum + e.amount, 0),
      byType: this.groupByType(entries),
      byStatus: this.groupByStatus(entries),
      byToken: this.groupByToken(entries)
    };
  }
}

Access Control

typescript
/**
 * Wallet Access Control - Controle de acesso a wallets
 */
class WalletAccessControl {
  async canAccess(userId: UserId, walletId: WalletId): Promise<boolean> {
    const wallet = await this.walletRepository.findById(walletId);
    
    if (!wallet) return false;

    // Owner sempre pode acessar
    if (wallet.owner.equals(userId)) return true;

    // Admin pode acessar qualquer wallet
    if (await this.isAdmin(userId)) return true;

    // Verificar permissões delegadas
    const permission = await this.permissionRepository.findByUserAndWallet(
      userId,
      walletId
    );

    return permission?.canRead || false;
  }

  async canTransfer(
    userId: UserId,
    fromWallet: WalletId,
    toWallet: WalletId
  ): Promise<boolean> {
    const wallet = await this.walletRepository.findById(fromWallet);
    
    if (!wallet) return false;

    // Owner pode transferir
    if (wallet.owner.equals(userId)) return true;

    // Verificar permissão de transferência
    const permission = await this.permissionRepository.findByUserAndWallet(
      userId,
      fromWallet
    );

    return permission?.canTransfer || false;
  }
}

/**
 * Transaction Authorization
 */
class TransactionAuthorizer {
  async authorize(
    transaction: Transaction,
    initiator: UserId
  ): Promise<AuthorizationResult> {
    // 1. Verificar access control
    const canAccess = await this.walletAccessControl.canAccess(
      initiator,
      transaction.from || transaction.to
    );

    if (!canAccess) {
      return AuthorizationResult.denied('Insufficient permissions');
    }

    // 2. Verificar limites (rate limiting, amount limits)
    const limitsCheck = await this.checkLimits(transaction, initiator);
    
    if (!limitsCheck.passed) {
      return AuthorizationResult.denied(limitsCheck.reason);
    }

    // 3. Verificar compliance (KYC, AML)
    const complianceCheck = await this.checkCompliance(transaction);
    
    if (!complianceCheck.passed) {
      return AuthorizationResult.denied(complianceCheck.reason);
    }

    // 4. Tudo OK
    return AuthorizationResult.allowed();
  }

  private async checkLimits(
    transaction: Transaction,
    initiator: UserId
  ): Promise<LimitCheckResult> {
    // Limite por transação
    const maxPerTransaction = TokenAmount.create(1000);
    if (transaction.amount.isGreaterThan(maxPerTransaction)) {
      return LimitCheckResult.failed('Amount exceeds maximum per transaction');
    }

    // Limite diário
    const todayTotal = await this.getTodayTotal(initiator, transaction.token);
    const dailyLimit = TokenAmount.create(5000);
    
    if (todayTotal.add(transaction.amount).isGreaterThan(dailyLimit)) {
      return LimitCheckResult.failed('Daily limit exceeded');
    }

    return LimitCheckResult.passed();
  }

  private async checkCompliance(
    transaction: Transaction
  ): Promise<ComplianceCheckResult> {
    // Para tokens de alto valor, verificar compliance
    if (transaction.amount.isGreaterThan(TokenAmount.create(10000))) {
      // Verificar KYC do usuário
      // Verificar watchlists
      // etc.
    }

    return ComplianceCheckResult.passed();
  }
}

Fraud Detection

typescript
/**
 * Fraud Detection Service
 */
class FraudDetectionService {
  constructor(
    private readonly transactionRepository: TransactionRepository,
    private readonly userRepository: UserRepository
  ) {}

  async analyzeTransaction(
    transaction: Transaction
  ): Promise<FraudAnalysisResult> {
    const signals: FraudSignal[] = [];

    // 1. Verificar padrões suspeitos
    const patterns = await this.detectSuspiciousPatterns(transaction);
    signals.push(...patterns);

    // 2. Verificar velocidade (muitas transações em pouco tempo)
    const velocity = await this.checkVelocity(transaction);
    if (velocity.isSuspicious) {
      signals.push(velocity.signal);
    }

    // 3. Verificar valores anômalos
    const anomaly = await this.detectAnomalies(transaction);
    if (anomaly.isAnomaly) {
      signals.push(anomaly.signal);
    }

    // 4. Calcular risk score
    const riskScore = this.calculateRiskScore(signals);

    // 5. Decidir ação
    if (riskScore > 0.8) {
      return FraudAnalysisResult.block('High fraud risk');
    } else if (riskScore > 0.5) {
      return FraudAnalysisResult.review('Medium fraud risk');
    } else {
      return FraudAnalysisResult.allow();
    }
  }

  private async detectSuspiciousPatterns(
    transaction: Transaction
  ): Promise<FraudSignal[]> {
    const signals: FraudSignal[] = [];

    // Padrão: Transferências circulares (A → B → C → A)
    const isCircular = await this.isCircularTransfer(transaction);
    if (isCircular) {
      signals.push(FraudSignal.circularTransfer());
    }

    // Padrão: Conta nova com alto volume
    const isNewAccount = await this.isNewAccount(transaction.from!);
    const isHighVolume = transaction.amount.isGreaterThan(TokenAmount.create(1000));
    
    if (isNewAccount && isHighVolume) {
      signals.push(FraudSignal.newAccountHighVolume());
    }

    return signals;
  }

  private async checkVelocity(
    transaction: Transaction
  ): Promise<VelocityCheck> {
    const recentTransactions = await this.transactionRepository.findRecent(
      transaction.from!,
      Duration.fromMinutes(10)
    );

    // Mais de 10 transações em 10 minutos é suspeito
    if (recentTransactions.length > 10) {
      return VelocityCheck.suspicious(
        FraudSignal.highVelocity(recentTransactions.length)
      );
    }

    return VelocityCheck.normal();
  }

  private calculateRiskScore(signals: FraudSignal[]): number {
    if (signals.length === 0) return 0;

    const weights = {
      [FraudSignalType.CIRCULAR_TRANSFER]: 0.4,
      [FraudSignalType.HIGH_VELOCITY]: 0.3,
      [FraudSignalType.NEW_ACCOUNT_HIGH_VOLUME]: 0.5,
      [FraudSignalType.ANOMALY]: 0.2
    };

    const score = signals.reduce((sum, signal) => {
      return sum + (weights[signal.type] || 0.1);
    }, 0);

    return Math.min(score, 1.0); // Cap at 1.0
  }
}

Integrações

Antóctica Ecosystem

typescript
/**
 * ApplicationWallet - Integração com outras apps do ecossistema
 */
interface AntocticaIntegration {
  applicationId: string;
  applicationName: string;
  walletId: WalletId;
  apiKey: string;
  webhookUrl: string;
}

/**
 * Cross-Application Transfer
 */
class CrossApplicationTransferUseCase {
  async execute(
    request: CrossApplicationTransferRequest
  ): Promise<CrossApplicationTransferResponse> {
    // 1. Validar aplicação externa
    const externalApp = await this.applicationRepository.findById(
      request.targetApplicationId
    );

    if (!externalApp || !externalApp.isActive) {
      throw new ApplicationNotFoundError();
    }

    // 2. Obter ApplicationWallet da app externa
    let appWallet = await this.walletRepository.findByOwnerAndType(
      externalApp.id,
      WalletTypeVO.application()
    );

    if (!appWallet) {
      appWallet = Wallet.create({
        owner: externalApp.id,
        type: WalletTypeVO.application(),
        metadata: WalletMetadata.forApplication(externalApp.id)
      });
      await this.walletRepository.save(appWallet);
    }

    // 3. Executar transferência
    const transfer = await this.transferTokensUseCase.execute({
      fromWallet: request.fromWallet,
      toWallet: appWallet.id,
      amount: request.amount,
      token: request.token,
      reason: TransactionReason.CROSS_APP_TRANSFER,
      metadata: TransactionMetadata.forCrossAppTransfer(
        request.targetApplicationId
      )
    });

    // 4. Notificar aplicação externa via webhook
    await this.notifyExternalApplication(externalApp, transfer);

    return {
      transferId: transfer.transactionId,
      targetWallet: appWallet.id
    };
  }

  private async notifyExternalApplication(
    app: Application,
    transfer: TransferTokensResponse
  ): Promise<void> {
    await this.httpClient.post(app.webhookUrl, {
      event: 'tokens.received',
      data: {
        transactionId: transfer.transactionId,
        amount: transfer.amount,
        token: transfer.token
      },
      signature: this.generateSignature(app.apiKey, transfer)
    });
  }
}

Payment Gateway Integration (Conversão Real)

typescript
/**
 * ConvertRealToCrypto - Conversão de dinheiro real para ArenaCoin
 * (se permitido pela arena)
 */
class ConvertRealToTokenUseCase {
  constructor(
    private readonly paymentGateway: PaymentGateway,
    private readonly walletRepository: WalletRepository,
    private readonly conversionRateService: ConversionRateService
  ) {}

  async execute(
    request: ConvertRealToTokenRequest
  ): Promise<ConvertRealToTokenResponse> {
    // 1. Validar se conversão é permitida
    const tokenDef = await this.tokenDefinitionRepository.findById(
      request.token
    );

    if (!tokenDef!.rules.conversionRules.length) {
      throw new BusinessRuleError('Token does not allow real money conversion');
    }

    // 2. Obter taxa de conversão
    const rate = await this.conversionRateService.getRate(
      request.currency,
      request.token
    );

    const tokenAmount = rate.convert(request.amount);

    // 3. Processar pagamento real
    const payment = await this.paymentGateway.charge({
      amount: request.amount,
      currency: request.currency,
      paymentMethod: request.paymentMethod,
      description: `Purchase ${tokenAmount.toNumber()} ${tokenDef!.symbol}`
    });

    if (!payment.success) {
      throw new PaymentFailedError(payment.error);
    }

    // 4. Creditar tokens na wallet
    const creditTransaction = CreditTransaction.create({
      to: request.wallet,
      amount: tokenAmount,
      token: request.token,
      reason: TransactionReason.PURCHASE,
      metadata: TransactionMetadata.forPurchase(payment.id)
    });

    await creditTransaction.execute(
      this.walletRepository,
      this.ruleEngine
    );

    await this.transactionRepository.save(creditTransaction);

    // 5. Retornar
    return {
      paymentId: payment.id,
      transactionId: creditTransaction.id,
      tokenAmount,
      rate: rate.value
    };
  }
}

Implementação Técnica

Database Schema (Prisma)

prisma
// schema.prisma

model Wallet {
  id        String   @id @default(uuid())
  ownerId   String
  ownerType String   // USER, ARENA, EVENT, APPLICATION
  type      String   // WalletType enum
  status    String   @default("ACTIVE")
  metadata  Json?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  balances TokenBalance[]
  outgoingTransactions Transaction[] @relation("FromWallet")
  incomingTransactions Transaction[] @relation("ToWallet")

  @@unique([ownerId, type])
  @@index([ownerId])
  @@index([type])
}

model TokenBalance {
  id                String   @id @default(uuid())
  walletId          String
  tokenDefinitionId String
  available         Decimal  @default(0)
  locked            Decimal  @default(0)
  lastUpdated       DateTime @default(now())

  wallet          Wallet          @relation(fields: [walletId], references: [id])
  tokenDefinition TokenDefinition @relation(fields: [tokenDefinitionId], references: [id])

  @@unique([walletId, tokenDefinitionId])
  @@index([walletId])
}

model Transaction {
  id          String    @id @default(uuid())
  type        String    // CREDIT, DEBIT, TRANSFER, REWARD, BET, etc.
  fromWallet  String?
  toWallet    String
  amount      Decimal
  token       String
  status      String    // PENDING, COMPLETED, FAILED
  reason      String
  metadata    Json?
  createdAt   DateTime  @default(now())
  completedAt DateTime?

  from Wallet? @relation("FromWallet", fields: [fromWallet], references: [id])
  to   Wallet   @relation("ToWallet", fields: [toWallet], references: [id])

  @@index([fromWallet])
  @@index([toWallet])
  @@index([type])
  @@index([status])
  @@index([createdAt])
}

model TokenDefinition {
  id          String   @id @default(uuid())
  name        String   @unique
  symbol      String   @unique
  description String
  rules       Json     // TokenRules
  isActive    Boolean  @default(true)
  createdAt   DateTime @default(now())

  balances TokenBalance[]
}

model EventStoreEntry {
  id            String   @id @default(uuid())
  aggregateId   String
  aggregateType String
  eventType     String
  eventData     Json
  metadata      Json?
  occurredAt    DateTime
  version       Int

  @@index([aggregateId, version])
  @@index([eventType])
  @@index([occurredAt])
}

model TransactionLedger {
  id            String    @id @default(uuid())
  transactionId String    @unique
  type          String
  from          String?
  to            String
  amount        Decimal
  token         String
  status        String
  reason        String
  metadata      Json?
  createdAt     DateTime
  completedAt   DateTime?

  @@index([from])
  @@index([to])
  @@index([type])
  @@index([createdAt])
}

API Endpoints (REST)

typescript
// routes/wallet.routes.ts

/**
 * POST /api/wallets
 * Criar nova carteira
 */
router.post('/wallets', async (req, res) => {
  const result = await createWalletUseCase.execute({
    owner: OwnerId.create(req.body.ownerId),
    type: WalletTypeVO.create(req.body.type),
    metadata: req.body.metadata
  });

  res.status(201).json(result);
});

/**
 * GET /api/wallets/:id
 * Obter detalhes da carteira
 */
router.get('/wallets/:id', async (req, res) => {
  const wallet = await walletRepository.findById(
    WalletId.create(req.params.id)
  );

  res.json(WalletMapper.toDto(wallet));
});

/**
 * GET /api/wallets/:id/balance
 * Obter saldo da carteira
 */
router.get('/wallets/:id/balance', async (req, res) => {
  const wallet = await walletRepository.findById(
    WalletId.create(req.params.id)
  );

  const balances = wallet.balances.map(balance => ({
    token: balance.tokenDefinition,
    available: balance.available.toNumber(),
    locked: balance.locked.toNumber(),
    total: balance.total.toNumber()
  }));

  res.json({ balances });
});

/**
 * GET /api/wallets/:id/transactions
 * Obter histórico de transações
 */
router.get('/wallets/:id/transactions', async (req, res) => {
  const history = await transactionLedger.getHistory(
    WalletId.create(req.params.id),
    {
      type: req.query.type,
      from: req.query.from,
      to: req.query.to,
      limit: parseInt(req.query.limit || '50'),
      offset: parseInt(req.query.offset || '0')
    }
  );

  res.json({ transactions: history });
});

/**
 * POST /api/transactions/transfer
 * Transferir tokens entre wallets
 */
router.post('/transactions/transfer', async (req, res) => {
  const result = await transferTokensUseCase.execute({
    fromWallet: WalletId.create(req.body.from),
    toWallet: WalletId.create(req.body.to),
    amount: TokenAmount.create(req.body.amount),
    token: TokenDefinitionId.create(req.body.token),
    reason: TransactionReason.create(req.body.reason),
    metadata: req.body.metadata
  });

  res.status(201).json(result);
});

/**
 * POST /api/transactions/bet
 * Apostar em partida
 */
router.post('/transactions/bet', async (req, res) => {
  const result = await placeBetUseCase.execute({
    userWallet: WalletId.create(req.body.walletId),
    matchId: MatchId.create(req.body.matchId),
    amount: TokenAmount.create(req.body.amount),
    token: TokenDefinitionId.create(req.body.token),
    prediction: BetPrediction.fromDto(req.body.prediction)
  });

  res.status(201).json(result);
});

/**
 * POST /api/transactions/exchange
 * Trocar tokens por serviço/produto
 */
router.post('/transactions/exchange', async (req, res) => {
  const result = await exchangeTokensUseCase.execute({
    wallet: WalletId.create(req.body.walletId),
    amount: TokenAmount.create(req.body.amount),
    token: TokenDefinitionId.create(req.body.token),
    exchangeType: ExchangeType.create(req.body.exchangeType)
  });

  res.status(201).json(result);
});

WebSocket Real-Time Updates

typescript
// websocket/wallet.gateway.ts

@WebSocketGateway()
export class WalletGateway {
  @WebSocketServer()
  server: Server;

  constructor(private readonly eventBus: EventBus) {
    // Subscribe to wallet events
    this.eventBus.subscribe(TokensCreditedEvent, this.onTokensCredited.bind(this));
    this.eventBus.subscribe(TokensDebitedEvent, this.onTokensDebited.bind(this));
    this.eventBus.subscribe(TransactionCompletedEvent, this.onTransactionCompleted.bind(this));
  }

  @SubscribeMessage('subscribe:wallet')
  handleSubscribeWallet(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: { walletId: string }
  ) {
    // Add client to room
    client.join(`wallet:${data.walletId}`);
  }

  @SubscribeMessage('unsubscribe:wallet')
  handleUnsubscribeWallet(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: { walletId: string }
  ) {
    client.leave(`wallet:${data.walletId}`);
  }

  private async onTokensCredited(event: TokensCreditedEvent) {
    this.server.to(`wallet:${event.walletId}`).emit('balance:updated', {
      walletId: event.walletId,
      token: event.token,
      amount: event.amount,
      type: 'credit'
    });
  }

  private async onTokensDebited(event: TokensDebitedEvent) {
    this.server.to(`wallet:${event.walletId}`).emit('balance:updated', {
      walletId: event.walletId,
      token: event.token,
      amount: event.amount,
      type: 'debit'
    });
  }

  private async onTransactionCompleted(event: TransactionCompletedEvent) {
    // Notify both sender and receiver
    this.server.to(`wallet:${event.fromWallet}`).emit('transaction:completed', {
      transactionId: event.transactionId,
      type: event.type,
      status: 'completed'
    });

    this.server.to(`wallet:${event.toWallet}`).emit('transaction:completed', {
      transactionId: event.transactionId,
      type: event.type,
      status: 'completed'
    });
  }
}

Métricas e Observabilidade

typescript
/**
 * Wallet Metrics
 */
class WalletMetrics {
  async getTotalCirculation(token: TokenDefinitionId): Promise<TokenAmount> {
    const result = await this.db.$queryRaw`
      SELECT SUM(available + locked) as total
      FROM TokenBalance
      WHERE tokenDefinitionId = ${token.value}
    `;

    return TokenAmount.create(result[0].total || 0);
  }

  async getActiveWallets(): Promise<number> {
    return this.db.wallet.count({
      where: { status: 'ACTIVE' }
    });
  }

  async getTransactionVolume(
    period: Duration
  ): Promise<TransactionVolumeMetrics> {
    const since = DateTime.now().subtract(period);

    const result = await this.db.transaction.aggregate({
      where: {
        createdAt: { gte: since.toDate() },
        status: 'COMPLETED'
      },
      _sum: { amount: true },
      _count: true
    });

    return {
      totalAmount: TokenAmount.create(result._sum.amount || 0),
      totalCount: result._count,
      period: period.toString()
    };
  }

  async getTopHolders(
    token: TokenDefinitionId,
    limit: number = 10
  ): Promise<WalletBalance[]> {
    const balances = await this.db.tokenBalance.findMany({
      where: { tokenDefinitionId: token.value },
      orderBy: { available: 'desc' },
      take: limit,
      include: { wallet: true }
    });

    return balances.map(b => ({
      walletId: b.walletId,
      owner: b.wallet.ownerId,
      balance: TokenAmount.create(b.available)
    }));
  }
}

Considerações Finais

Próximos Passos

  1. Implementar Sistema de Níveis (Gamificação)

    • Bronze, Silver, Gold, Platinum
    • Benefícios por nível
    • Evolução baseada em atividade
  2. Marketplace de Tokens

    • P2P trading (se permitido)
    • Auction system
    • Price discovery
  3. Mobile Wallet

    • App nativo iOS/Android
    • QR Code scanning
    • Push notifications
  4. Analytics Dashboard

    • Token economics
    • User behavior
    • Fraud detection
  5. Smart Contracts (Futuro)

    • Migração para blockchain real
    • Descentralização
    • Interoperabilidade

Performance Considerations

  • Caching: Redis para balanços frequentes
  • Read Replicas: Queries de leitura em replicas
  • CQRS: Separação de write/read models
  • Event Sourcing: Rebuild state from events
  • Partitioning: Sharding por wallet ranges

Compliance

  • KYC/AML: Para conversões > threshold
  • GDPR: Right to erasure (event anonymization)
  • SOC 2: Audit trails
  • PCI-DSS: Payment data handling

"In code we trust, but we verify with events." — Domain-Driven Design Community


Documento criado por: Engenheiro (Arquiteto de Software Sênior)
Data: 2026-01-09
Versão: 1.0